Part 01 · Low Level Design
Observer and Event-Driven Design
একটা পরিবর্তন যখন অনেক object এর কাছে পৌঁছাতে হয়: Observer, সবাই সবার সাথে কথা বলা বন্ধ করা Mediator, আর আসল broker আসার আগে process এর ভেতরের event bus।
⏱ 1-2 ঘণ্টা · Station 10 / 80
PlaceOrder অনেক বেশি মানুষকে চেনে
Customer order দিলে দোকানকে একটা SMS পাঠাতে হয়, loyalty point যোগ করতে হয়, analytics এ লিখতে হয়, warehouse কে জানাতে হয়, আর invoice email করতে হয়। PlaceOrder এর প্রথম version পাঁচটা service কেই সরাসরি ডাকে, একটার পর একটা।
তারপর সমস্যা জমতে থাকে। SMS gateway down হলো আর order ই fail করতে লাগলো, কারণ প্রথম error পেলেই PlaceOrder থেমে যায়। Marketing নতুন একটা "first order" coupon চায়, আর কাউকে আবার PlaceOrder edit করতে হয়। Analytics team এক সপ্তাহের জন্য তাদের tracking বন্ধ রাখতে চায়, সেটাও আরেকটা edit। PlaceOrder হয়ে গেছে এমন একটা function যেটা সবাইকে বদলাতে হয়, আর যে সবাইকে চেনে।
সমাধান হলো কে কাকে চেনে সেটা উল্টে দেওয়া: order শুধু ঘোষণা করে "একটা order হয়েছে", আর যার দরকার সে শোনে। এটাই Observer pattern, আর প্রতিটা event-driven system এর পেছনের idea।
সমস্যা: caller প্রতিটা reaction কে চেনে
| যা দেখা যায় | কারণ |
|---|---|
| নতুন reaction (coupon, warehouse) যোগ করতে মূল flow edit করতে হয় | মূল flow প্রতিটা reaction কে সরাসরি ডাকে |
| একটা reaction fail করলে (SMS) পুরো order ভেঙে যায় | reaction গুলো মূল flow এর ভেতরেই চলে, কোনো আলাদা করা নেই |
| Deploy ছাড়া কোনো reaction বন্ধ করা যায় না | reaction গুলো শক্ত করে বাঁধা |
| মূল service পাঁচটা অন্য module import করে | Dependency গুলো মূল থেকে বাইরের দিকে |
মসজিদের মাইক আর একটা WhatsApp group এর কথা ভাবো
মুয়াজ্জিন প্রতিটা বাসায় phone করে বলেন না যে নামাজের সময় হয়েছে। একটা ঘোষণা যায়, আর যে শুনতে চায় সে শোনে। নতুন পরিবার এলাকায় আসলে তারাও শোনে; মুয়াজ্জিনের কাজ বদলায় না। এটা Observer: একটা subject, অনেক শ্রোতা, আর subject কে কারা কী করছে তার list রাখতে হয় না।
একটা WhatsApp group হলো mediator: member দের একে অপরের নম্বর লাগে না; তারা group এ কথা বলে, group পৌঁছে দেয়। Event bus তোমার program এর ভেতরে এই একই জিনিস: publisher আর subscriber রা চেনে শুধু bus আর topic এর নাম।
তবে একটা জায়গায় মেলে না। ঘোষণা মিস করলে মিস হয়ে যায়, ব্যাস। Software এ একটা event মিস করা মানে হতে পারে invoice যায়নি বা point হারিয়েছে, তাই ঠিক করতে হয়: "process এর ভেতরে, পারলে পৌঁছাবে" যথেষ্ট কিনা, নাকি একটা durable queue লাগবে (HLD এর chapter গুলোতে) যেটা event ততক্ষণ রেখে দেয় যতক্ষণ না কেউ সেটা সামলায়।
Order service 'order.placed' একটা bus এ publish করে। SMS, loyalty আর analytics subscribe করে আছে। Marketing একটা 'first order coupon' reaction যোগ করলো। কোন code বদলাবে?
Observer, mediator আর event bus
- Observer: একটা subject observer দের একটা list রাখে, আর কিছু ঘটলে প্রত্যেককে জানায়। Observer রা নিজেরাই register আর unregister করে। Subject নির্ভর করে শুধু ছোট একটা interface এর উপর ("এমন কিছু যাকে জানানো যায়"), নির্দিষ্ট reaction গুলোর উপর না।
- Event: অতীতের একটা ঘটনা, অতীত কালে নাম দেওয়া:
order.placed,payment.failed। reaction এর যা data লাগে সেটা বহন করে (order ID, customer, amount), আর কে reaction দেবে সে ব্যাপারে কিছুই না। - Event bus (in-process pub/sub): একটা shared object, যেখানে publisher একটা topic এ publish করে আর subscriber রা topic এ subscribe করে। কোনো দিকই অন্য দিককে চেনে না। এটা মাঝখানে একজন লোক সহ Observer, তাই অনেক subject আর অনেক observer প্রতিটা জোড়া আলাদা করে না জুড়েই মিলতে পারে।
- Mediator: এমন একটা object যেটা একদল object এর মধ্যে সমন্বয় করে, যাতে তারা সরাসরি একে অপরের সাথে কথা না বলে। Chat room, air-traffic controller, ride-matching service: প্রত্যেকে mediator এর সাথে কথা বলে। Bus হলো event এর জন্য সাধারণ একটা mediator; mediator সমন্বয়ের logic ও রাখতে পারে।
- আলাদা রাখা: একটা subscriber fail করলে বাকিরা বা publisher যেন থেমে না যায়। প্রতিটা subscriber এর error আলাদা করে ধরো, log করো, আর retry করবে কিনা ঠিক করো।
- দাম: flow টা বোঝা কঠিন হয়ে যায়। "Order হলে কী কী হয়?" এর উত্তর আর একটা function না; উত্তর হলো "
order.placedএ যা যা subscribe করা"। Subscription গুলো এমন এক জায়গায় register করো যেটা চোখে পড়ে।
একটা event, অনেক reaction
bus.Publish(order.placed,
ধাপ 1/4 Order service একটা ঘটনা ঘোষণা করে
রহিমের ৩০০ টাকার order #503 হয়েছে। Service publish করে, তারপর তার কাজ শেষ। কে শুনছে সে জানে না।
- topic
- order.placed
- order
- #503
কে কাকে চেনে?
চেনাজানার তীর গুলো আঁকো:
- সরাসরি call: order → SMS, order → loyalty, order → analytics। মূল flow সবাইকে চেনে।
- Event দিয়ে: order → bus ← SMS, loyalty, analytics। মূল flow চেনে শুধু bus কে; reaction গুলো চেনে শুধু bus আর topic।
নতুন feature যোগ করতে যদি মূল flow edit করতে হয় শুধু নতুন কিছু ডাকার জন্য, জিজ্ঞেস করো এটা বরং একটা subscriber হওয়া উচিত কিনা।
নিজে বলো: event না সরাসরি call?
এটা কি event হওয়া উচিত?
publish(user.registered)
Code এ order এর event bus
Topic, subscribe/unsubscribe, আর প্রতিটা subscriber এর error আলাদা রাখা সহ ছোট একটা in-process bus। তিনটা subscriber order এ সাড়া দেয়, একটা পরে unsubscribe করে, আর একটা fail করে।
package main
import (
"errors"
"fmt"
)
type Event struct {
OrderID int
Customer string
Amount int // taka
}
type Handler func(e Event) error
type subscription struct {
id int
name string
fn Handler
}
// Bus is an in-process event bus: publishers and subscribers only know the bus and the topic.
type Bus struct {
subs map[string][]subscription
nextID int
}
func NewBus() *Bus { return &Bus{subs: map[string][]subscription{}} }
func (b *Bus) Subscribe(topic, name string, fn Handler) int {
b.nextID++
b.subs[topic] = append(b.subs[topic], subscription{b.nextID, name, fn})
return b.nextID
}
func (b *Bus) Unsubscribe(id int) {
for topic, list := range b.subs {
kept := list[:0:0]
for _, s := range list {
if s.id != id {
kept = append(kept, s)
}
}
b.subs[topic] = kept
}
}
// Publish calls every subscriber in order. One failing subscriber doesn't stop the others.
func (b *Bus) Publish(topic string, e Event) {
fmt.Printf("publish %s #%d\n", topic, e.OrderID)
for _, s := range b.subs[topic] {
if err := s.fn(e); err != nil {
fmt.Printf(" %-9s : failed: %v\n", s.name, err)
}
}
}
func main() {
bus := NewBus()
// The order service only publishes. It doesn't know who listens.
bus.Subscribe("order.placed", "sms", func(e Event) error {
if e.OrderID == 503 {
return errors.New("gateway down")
}
fmt.Printf(" %-9s : sent \"order #%d confirmed\" to %s\n", "sms", e.OrderID, e.Customer)
return nil
})
points := map[string]int{}
loyalty := func(sign int) Handler {
return func(e Event) error {
p := sign * (e.Amount / 100) // 1 point per 100 taka
points[e.Customer] += p
fmt.Printf(" %-9s : %s %+d points (total %d)\n", "loyalty", e.Customer, p, points[e.Customer])
return nil
}
}
bus.Subscribe("order.placed", "loyalty", loyalty(+1))
bus.Subscribe("order.cancelled", "loyalty", loyalty(-1))
analytics := bus.Subscribe("order.placed", "analytics", func(e Event) error {
fmt.Printf(" %-9s : recorded #%d\n", "analytics", e.OrderID)
return nil
})
bus.Publish("order.placed", Event{501, "Rahim", 720})
bus.Publish("order.placed", Event{502, "Nadia", 1500})
bus.Unsubscribe(analytics)
fmt.Println("analytics unsubscribed")
bus.Publish("order.placed", Event{503, "Rahim", 300})
bus.Publish("order.cancelled", Event{502, "Nadia", 1500})
}from __future__ import annotations
from dataclasses import dataclass
from typing import Callable
@dataclass(frozen=True)
class Event:
order_id: int
customer: str
amount: int # taka
Handler = Callable[[Event], None]
class Bus:
"""An in-process event bus: publishers and subscribers only know the bus and the topic."""
def __init__(self) -> None:
self._subs: dict[str, list[tuple[int, str, Handler]]] = {}
self._next_id = 0
def subscribe(self, topic: str, name: str, fn: Handler) -> int:
self._next_id += 1
self._subs.setdefault(topic, []).append((self._next_id, name, fn))
return self._next_id
def unsubscribe(self, sub_id: int) -> None:
for topic, subs in self._subs.items():
self._subs[topic] = [s for s in subs if s[0] != sub_id]
def publish(self, topic: str, e: Event) -> None:
"""Calls every subscriber in order. One failing subscriber doesn't stop the others."""
print(f"publish {topic} #{e.order_id}")
for _, name, fn in self._subs.get(topic, []):
try:
fn(e)
except Exception as err:
print(f" {name:<9} : failed: {err}")
bus = Bus()
# The order service only publishes. It doesn't know who listens.
def sms(e: Event) -> None:
if e.order_id == 503:
raise RuntimeError("gateway down")
print(f' {"sms":<9} : sent "order #{e.order_id} confirmed" to {e.customer}')
bus.subscribe("order.placed", "sms", sms)
points: dict[str, int] = {}
def loyalty(sign: int) -> Handler:
def handle(e: Event) -> None:
p = sign * (e.amount // 100) # 1 point per 100 taka
points[e.customer] = points.get(e.customer, 0) + p
print(f" {'loyalty':<9} : {e.customer} {p:+d} points (total {points[e.customer]})")
return handle
bus.subscribe("order.placed", "loyalty", loyalty(+1))
bus.subscribe("order.cancelled", "loyalty", loyalty(-1))
analytics = bus.subscribe("order.placed", "analytics",
lambda e: print(f" {'analytics':<9} : recorded #{e.order_id}"))
bus.publish("order.placed", Event(501, "Rahim", 720))
bus.publish("order.placed", Event(502, "Nadia", 1500))
bus.unsubscribe(analytics)
print("analytics unsubscribed")
bus.publish("order.placed", Event(503, "Rahim", 300))
bus.publish("order.cancelled", Event(502, "Nadia", 1500))interface OrderEvent {
readonly orderId: number;
readonly customer: string;
readonly amount: number; // taka
}
type Handler = (e: OrderEvent) => void;
// An in-process event bus: publishers and subscribers only know the bus and the topic.
class Bus {
readonly #subs = new Map<string, { id: number; name: string; fn: Handler }[]>();
#nextId = 0;
subscribe(topic: string, name: string, fn: Handler): number {
const id = ++this.#nextId;
this.#subs.set(topic, [...(this.#subs.get(topic) ?? []), { id, name, fn }]);
return id;
}
unsubscribe(id: number): void {
for (const [topic, list] of this.#subs) this.#subs.set(topic, list.filter((s) => s.id !== id));
}
// Calls every subscriber in order. One failing subscriber doesn't stop the others.
publish(topic: string, e: OrderEvent): void {
console.log(`publish ${topic} #${e.orderId}`);
for (const s of this.#subs.get(topic) ?? []) {
try {
s.fn(e);
} catch (err) {
console.log(` ${s.name.padEnd(9)} : failed: ${(err as Error).message}`);
}
}
}
}
const bus = new Bus();
// The order service only publishes. It doesn't know who listens.
bus.subscribe("order.placed", "sms", (e) => {
if (e.orderId === 503) throw new Error("gateway down");
console.log(` ${"sms".padEnd(9)} : sent "order #${e.orderId} confirmed" to ${e.customer}`);
});
const points = new Map<string, number>();
const loyalty =
(sign: number): Handler =>
(e) => {
const p = sign * Math.floor(e.amount / 100); // 1 point per 100 taka
points.set(e.customer, (points.get(e.customer) ?? 0) + p);
const signed = p >= 0 ? `+${p}` : `${p}`;
console.log(` ${"loyalty".padEnd(9)} : ${e.customer} ${signed} points (total ${points.get(e.customer)})`);
};
bus.subscribe("order.placed", "loyalty", loyalty(+1));
bus.subscribe("order.cancelled", "loyalty", loyalty(-1));
const analytics = bus.subscribe("order.placed", "analytics", (e) =>
console.log(` ${"analytics".padEnd(9)} : recorded #${e.orderId}`),
);
bus.publish("order.placed", { orderId: 501, customer: "Rahim", amount: 720 });
bus.publish("order.placed", { orderId: 502, customer: "Nadia", amount: 1500 });
bus.unsubscribe(analytics);
console.log("analytics unsubscribed");
bus.publish("order.placed", { orderId: 503, customer: "Rahim", amount: 300 });
bus.publish("order.cancelled", { orderId: 502, customer: "Nadia", amount: 1500 });use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
#[derive(Clone)]
struct Event { order_id: u32, customer: String, amount: i32 } // amount in taka
type Handler = Box<dyn Fn(&Event) -> Result<(), String>>;
// An in-process event bus: publishers and subscribers only know the bus and the topic.
#[derive(Default)]
struct Bus { subs: HashMap<String, Vec<(u32, String, Handler)>>, next_id: u32 }
impl Bus {
fn subscribe(&mut self, topic: &str, name: &str, f: Handler) -> u32 {
self.next_id += 1;
self.subs.entry(topic.into()).or_default().push((self.next_id, name.into(), f));
self.next_id
}
fn unsubscribe(&mut self, id: u32) {
for list in self.subs.values_mut() { list.retain(|(i, _, _)| *i != id); }
}
// Calls every subscriber in order. One failing subscriber doesn't stop the others.
fn publish(&self, topic: &str, e: &Event) {
println!("publish {topic} #{}", e.order_id);
for (_, name, f) in self.subs.get(topic).into_iter().flatten() {
if let Err(err) = f(e) { println!(" {name:<9} : failed: {err}"); }
}
}
}
fn main() {
let mut bus = Bus::default();
// The order service only publishes. It doesn't know who listens.
bus.subscribe("order.placed", "sms", Box::new(|e| {
if e.order_id == 503 { return Err("gateway down".into()); }
println!(" {:<9} : sent \"order #{} confirmed\" to {}", "sms", e.order_id, e.customer);
Ok(())
}));
// Two handlers share the points table, so it lives behind Rc<RefCell<...>>.
let points = Rc::new(RefCell::new(HashMap::<String, i32>::new()));
let loyalty = |sign: i32| -> Handler {
let points = Rc::clone(&points);
Box::new(move |e: &Event| {
let p = sign * (e.amount / 100); // 1 point per 100 taka
let mut pts = points.borrow_mut();
let total = pts.entry(e.customer.clone()).or_insert(0);
*total += p;
println!(" {:<9} : {} {:+} points (total {})", "loyalty", e.customer, p, total);
Ok(())
})
};
bus.subscribe("order.placed", "loyalty", loyalty(1));
bus.subscribe("order.cancelled", "loyalty", loyalty(-1));
let analytics = bus.subscribe("order.placed", "analytics", Box::new(|e| {
println!(" {:<9} : recorded #{}", "analytics", e.order_id);
Ok(())
}));
let ev = |id, c: &str, a| Event { order_id: id, customer: c.into(), amount: a };
bus.publish("order.placed", &ev(501, "Rahim", 720));
bus.publish("order.placed", &ev(502, "Nadia", 1500));
bus.unsubscribe(analytics);
println!("analytics unsubscribed");
bus.publish("order.placed", &ev(503, "Rahim", 300));
bus.publish("order.cancelled", &ev(502, "Nadia", 1500));
}import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
public class Main {
record Event(int orderId, String customer, int amount) {} // amount in taka
record Subscription(int id, String name, Consumer<Event> fn) {}
// An in-process event bus: publishers and subscribers only know the bus and the topic.
static final class Bus {
private final Map<String, List<Subscription>> subs = new HashMap<>();
private int nextId = 0;
int subscribe(String topic, String name, Consumer<Event> fn) {
subs.computeIfAbsent(topic, t -> new ArrayList<>()).add(new Subscription(++nextId, name, fn));
return nextId;
}
void unsubscribe(int id) {
for (List<Subscription> list : subs.values()) list.removeIf(s -> s.id() == id);
}
// Calls every subscriber in order. One failing subscriber doesn't stop the others.
void publish(String topic, Event e) {
System.out.printf("publish %s #%d%n", topic, e.orderId());
for (Subscription s : subs.getOrDefault(topic, List.of())) {
try {
s.fn().accept(e);
} catch (RuntimeException err) {
System.out.printf(" %-9s : failed: %s%n", s.name(), err.getMessage());
}
}
}
}
public static void main(String[] args) {
Bus bus = new Bus();
// The order service only publishes. It doesn't know who listens.
bus.subscribe("order.placed", "sms", e -> {
if (e.orderId() == 503) throw new IllegalStateException("gateway down");
System.out.printf(" %-9s : sent \"order #%d confirmed\" to %s%n", "sms", e.orderId(), e.customer());
});
Map<String, Integer> points = new HashMap<>();
java.util.function.IntFunction<Consumer<Event>> loyalty = sign -> e -> {
int p = sign * (e.amount() / 100); // 1 point per 100 taka
int total = points.merge(e.customer(), p, Integer::sum);
System.out.printf(" %-9s : %s %+d points (total %d)%n", "loyalty", e.customer(), p, total);
};
bus.subscribe("order.placed", "loyalty", loyalty.apply(1));
bus.subscribe("order.cancelled", "loyalty", loyalty.apply(-1));
int analytics = bus.subscribe("order.placed", "analytics",
e -> System.out.printf(" %-9s : recorded #%d%n", "analytics", e.orderId()));
bus.publish("order.placed", new Event(501, "Rahim", 720));
bus.publish("order.placed", new Event(502, "Nadia", 1500));
bus.unsubscribe(analytics);
System.out.println("analytics unsubscribed");
bus.publish("order.placed", new Event(503, "Rahim", 300));
bus.publish("order.cancelled", new Event(502, "Nadia", 1500));
}
}#include <algorithm>
#include <cstdio>
#include <functional>
#include <map>
#include <stdexcept>
#include <string>
#include <vector>
struct Event {
int orderId;
std::string customer;
int amount; // taka
};
using Handler = std::function<void(const Event&)>;
// An in-process event bus: publishers and subscribers only know the bus and the topic.
class Bus {
public:
int subscribe(const std::string& topic, const std::string& name, Handler fn) {
subs_[topic].push_back({++nextId_, name, std::move(fn)});
return nextId_;
}
void unsubscribe(int id) {
for (auto& [topic, list] : subs_) std::erase_if(list, [&](const Sub& s) { return s.id == id; });
}
// Calls every subscriber in order. One failing subscriber doesn't stop the others.
void publish(const std::string& topic, const Event& e) {
std::printf("publish %s #%d\n", topic.c_str(), e.orderId);
for (const auto& s : subs_[topic]) {
try {
s.fn(e);
} catch (const std::exception& err) {
std::printf(" %-9s : failed: %s\n", s.name.c_str(), err.what());
}
}
}
private:
struct Sub {
int id;
std::string name;
Handler fn;
};
std::map<std::string, std::vector<Sub>> subs_;
int nextId_ = 0;
};
int main() {
Bus bus;
// The order service only publishes. It doesn't know who listens.
bus.subscribe("order.placed", "sms", [](const Event& e) {
if (e.orderId == 503) throw std::runtime_error("gateway down");
std::printf(" %-9s : sent \"order #%d confirmed\" to %s\n", "sms", e.orderId, e.customer.c_str());
});
std::map<std::string, int> points;
auto loyalty = [&points](int sign) -> Handler {
return [&points, sign](const Event& e) {
int p = sign * (e.amount / 100); // 1 point per 100 taka
int total = (points[e.customer] += p);
std::printf(" %-9s : %s %+d points (total %d)\n", "loyalty", e.customer.c_str(), p, total);
};
};
bus.subscribe("order.placed", "loyalty", loyalty(1));
bus.subscribe("order.cancelled", "loyalty", loyalty(-1));
int analytics = bus.subscribe("order.placed", "analytics",
[](const Event& e) { std::printf(" %-9s : recorded #%d\n", "analytics", e.orderId); });
bus.publish("order.placed", {501, "Rahim", 720});
bus.publish("order.placed", {502, "Nadia", 1500});
bus.unsubscribe(analytics);
std::printf("analytics unsubscribed\n");
bus.publish("order.placed", {503, "Rahim", 300});
bus.publish("order.cancelled", {502, "Nadia", 1500});
}ছয়টাই হুবহু এটা print করে:
publish order.placed #501
sms : sent "order #501 confirmed" to Rahim
loyalty : Rahim +7 points (total 7)
analytics : recorded #501
publish order.placed #502
sms : sent "order #502 confirmed" to Nadia
loyalty : Nadia +15 points (total 15)
analytics : recorded #502
analytics unsubscribed
publish order.placed #503
sms : failed: gateway down
loyalty : Rahim +3 points (total 10)
publish order.cancelled #502
loyalty : Nadia -15 points (total 0)
Output টা পড়ি:
- #501 আর #502: প্রতিটা publish তিনজন subscriber এর কাছেই পৌঁছায়, যে ক্রমে তারা subscribe করেছিল সেই ক্রমে। Publisher কারো নাম নেয়নি।
- Point: প্রতি ১০০ টাকায় ১ point, তাই ৭২০ টাকায় ৭ point আর ১৫০০ তে ১৫।
- analytics unsubscribed: এখন থেকে তাকে আর ডাকা হয় না; আর কোনো code বদলায়নি।
- #503: SMS fail করে ("gateway down"), bus সেটা log করে, আর loyalty তবুও চলে: রহিম হয় ১০ point।
- order.cancelled #502: cancel এ subscribe করেছিল শুধু loyalty; সে নাদিয়ার ১৫ point ফেরত নেয়।
- Language ভেদে কী আলাদা: Go, TypeScript, Java আর C++ handler রাখে function বা lambda হিসেবে। Rust এ handler এর জন্য
Box<dyn Fn>লাগে, আর point এর table এর জন্যRc<RefCell<…>>, কারণ দুইটা handler এটা share করে আর বদলায়; borrow checker এই share করাটা খোলাখুলি লিখিয়ে নেয়। Python প্রতিটা subscriber এর যেকোনো exception ধরে; Go তে handler throw না করে একটাerrorফেরত দেয়।
Real system এ event
- In-process event হলো "ছুড়ে দাও, ভুলে যাও"। Publish এর পরে process crash করলে reaction গুলো হারিয়ে যায়। যা অবশ্যই হতে হবে (invoice, refund), তার জন্য order এর একই transaction এ event টা database এ লেখো (outbox pattern), আর সেখান থেকে পৌঁছাও।
- Sync না async, ঠিক করো। এই chapter এর bus subscriber দের একটা একটা করে, সাথে সাথে ডাকে। ধীর একটা subscriber publisher কেও ধীর করে দেয়। আসল system প্রায়ই event একটা worker বা queue এর হাতে দেয়।
- Subscriber কে idempotent বানাও। Event retry হলে একই event দুইবার আসতে পারে; দুইবার point দেওয়া একটা bug। Event বা order ID দিয়ে duplicate বাদ দাও।
- Subscription গুলো এক জায়গায় register করো। একটা
wiring.goবাmain, যেখানে প্রতিটা subscription এর list আছে, flow টা খুঁজে পাওয়ার মতো রাখে। - Event এর নাম দাও ঘটনা হিসেবে, অতীত কালে, কাজ করার মতো যথেষ্ট data সহ।
doStuff{id}এর চেয়েorder.placed{id, customer, amount}ভালো।
যে design গুলো পরে problem create করে
যে event design টা পরে ভোগাবে
for s := range subs { if err := s(e); err != nil { return err } }একটু থেমে ভাবো
Finance team বললো: "প্রতিটা order এর জন্য invoice email যেতেই হবে, কোনো ব্যতিক্রম নেই।" In-process একটা subscriber কি যথেষ্ট?
উত্তর দেখো
না। Order save হওয়া আর subscriber চলার মাঝখানে process crash করলে in-process bus event হারিয়ে ফেলে, আর fail করা একটা email শুধু log হয়। "কোনো ব্যতিক্রম নেই" এর জন্য order এর একই database transaction এ একটা invoice_pending record (বা outbox event) save করো, আর একটা worker সেই table থেকে invoice পাঠাবে, done মার্ক করবে, আর fail হলে retry করবে। Event এর idea একই থাকে; শুধু পৌঁছানোটা হয় টেকসই। HLD part এ queue আর outbox pattern বিস্তারিত আছে।
ছোট্ট quiz
১. Observer কে কার উপর নির্ভর করে, সেটা কীভাবে বদলায়?
উত্তর
Subject আর নির্দিষ্ট reaction গুলোর উপর নির্ভর করে না। Observer রা নির্ভর করে subject (বা bus) এর উপর, আর subject চেনে শুধু জানানোর ছোট একটা interface।
২. প্রতিটা subscriber এর error আলাদা রাখা কেন জরুরি?
উত্তর
যাতে একটা ভাঙা reaction (down হওয়া SMS gateway) বাকিদের বা publisher কে থামিয়ে না দেয়। প্রতিটা failure আলাদা করে log আর retry করা যায়।
৩. কখন event ভুল tool?
উত্তর
যখন caller কে সামনে যাওয়ার জন্য উত্তরের অপেক্ষা করতেই হয় (address check, card authorise)। সেটা সরাসরি call।
৪. Event bus আর mediator এর পার্থক্য কী?
উত্তর
Bus শুধু topic ধরে publisher থেকে subscriber এর কাছে event পৌঁছায়। Mediator একদল object এর সমন্বয়ের logic ও রাখতে পারে (chat room ঠিক করে কে message পাবে, dispatcher rider আর driver মেলায়)।
তোমার challenge: প্রথম order এ coupon
order.placed এ একটা coupon subscriber যোগ করো, যেটা শুধু customer এর প্রথম order এ coupon দেয়: কোনো customer কে প্রথমবার দেখলে print করে <customer> gets FIRST50 (বাকিদের মতো একই name : format এ), আর তারপর আর কিছু না।
- Analytics এর পরে, প্রথম publish এর আগে register করো।
- Bus বা আগের কোনো subscriber বদলাবে না।
চালানোর আগে আন্দাজ করো নতুন লাইন গুলো কোথায় আসবে।
হয়েছে বুঝবে যখন output এ ঠিক দুইটা লাইন যোগ হবে, #501 এর analytics লাইনের পরে coupon : Rahim gets FIRST50 আর #502 এর analytics লাইনের পরে coupon : Nadia gets FIRST50, #503 এ কোনো coupon লাইন আসবে না, আর বাকি সব লাইন একই থাকবে।
এক কথায়
প্রতিটা reaction কে ডেকো না, ঘটনা publish করো: publisher একটা topic এ ঘোষণা করে "এটা ঘটেছে", subscriber রা ঠিক করে কী করবে, আর কেউ কাউকে চেনে না। প্রতিটা subscriber এর failure আলাদা রাখো, subscription গুলো এক জায়গায় list করে রাখো, আর যে reaction কখনো হারানো চলবে না তার জন্য durable queue নাও।
Station stamp
Chapter শেষ করো
পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।
Pro নিয়ে XP পাও →