সরাসরি পর্বে যান
পর্ব ৩২ · ৬৬-এর মধ্যে · মডিউল ৬

Pub/sub আর event-driven architecture — fan-out, choreography বনাম orchestration

Pub/Sub and Event-Driven Architecture — Fan-out, Choreography vs Orchestration

Taught in Bangla, this lesson contrasts pub/sub with point-to-point queues, covers topics, subscriptions, push, pull and filters in a small broker simulator, separates events, commands and queries, designs good events with ids, envelopes and versions, works out fan-out cost, runs one flow as choreography and as orchestration, weighs the price of temporal decoupling, secures webhooks and lists event-driven anti-patterns.

এই পর্বে যা শিখবেন

  • Pub/sub আর point-to-point-এর পার্থক্য; topic, subscription, push, pull আর filter কীভাবে কাজ করে — একটি ছোট broker simulator-এ
  • Event, command আর query-র পার্থক্য, Fowler-এর তিন ধরনের event-ব্যবহার, আর একটি ভালো event-এর নাম, envelope ও version
  • একটি event-কে অনেক সেবায় পাঠানোর খরচ আর দেরি নিজে হিসাব করা
  • একই প্রবাহ choreography আর orchestration দুইভাবে চালিয়ে দেখা — কে কতটা জানে, coupling কোথায়
  • Temporal decoupling-এর দাম, নিরাপদ webhook, আর event-driven নকশার চেনা anti-pattern

১ · একজন বলে, অনেকে শোনে — pub/sub বনাম point-to-point

পর্ব ৩১-এ (message queue) একটি সারি দেখেছেন: কেউ কাজ জমা দেয়, আর কয়েকজন কর্মীর একজন সেটি তুলে নেয়। ব্যাংকের টোকেন-ব্যবস্থার মতো — একটি টোকেন একজন ক্যাশিয়ারই ডাকেন, দুজন নয়। এই ধরনের নাম point-to-point: প্রতিটি বার্তা ঠিক একজন গ্রাহকের কাছে যায়, আর কয়েকজন কর্মী মিলে ভার ভাগ করে (competing consumers).

এবার অন্য প্রশ্ন। একটি অনলাইন বাস-টিকিট সেবায় (কাল্পনিক) রুমানা টিকিট কিনলেন। এই একটি ঘটনার খবর অনেকের দরকার:

  • SMS সার্ভিস — রুমানাকে টিকিট নম্বর পাঠাবে;
  • Loyalty সার্ভিস — পয়েন্ট যোগ করবে;
  • Analytics — ঈদের ভিড়ের ড্যাশবোর্ড হালনাগাদ করবে;
  • আর আগামী মাসে হয়তো একটি fraud-check সার্ভিস, যার কথা আজ কেউ জানেই না।

Booking সার্ভিস যদি নিজে এদের একে একে ডাকে, তাহলে তাকে সবার ঠিকানা জানতে হয়, কেউ ধীর হলে সে-ও ধীর হয়, আর নতুন গ্রাহক এলে Booking-এর কোডই বদলাতে হয়। সেবাগুলো আলাদা দলের আলাদা microservice হলে (পর্ব ৪৭) এই বাঁধন আরও ভারী। সমাধান publish/subscribe: Booking শুধু একটি topic-এ (নামওয়ালা চ্যানেল) ঘোষণা দেয়, “টিকিট বুক হয়েছে”; যে শুনতে চায়, সে নিজের একটি subscription খোলে। Publisher জানে না কে শুনছে; প্রতিটি subscriber পায় নিজের কপি।

পত্রিকার কথা ভাবুন। প্রকাশক একবার ছাপেন; কোন বাড়ি পত্রিকা রাখে, তা তিনি জানেন না — জানেন হকার। প্রতিটি গ্রাহক বাড়ি নিজের কপি পায়; এক বাড়ি এক সপ্তাহ ঢাকার বাইরে থাকলে তার কপি দরজায় জমে, অন্যদের পত্রিকা দেরি করে না। নতুন কেউ পত্রিকা নিতে চাইলে প্রকাশককে কিছু বদলাতে হয় না, হকারের খাতায় একটি নাম যোগ হয়। প্রকাশক = publisher, পত্রিকার নাম = topic, হকারের খাতার একেকটি লাইন = subscription. আর ব্যাংকের টোকেন হলো point-to-point: একটি টোকেন, একজন ক্যাশিয়ার।

Topic, subscription আর fan-out

সবচেয়ে গুরুত্বপূর্ণ নিয়মটি হলো: প্রতিটি subscription একটি স্বাধীন গ্রাহক। একটি topic-এ তিনটি subscription থাকলে প্রতিটি event-এর তিনটি delivery হয় — এটিই বার্তার fan-out. আর একটি subscription-এর ভেতরে কয়েকটি কর্মী থাকলে তারা point-to-point-এর মতো ভার ভাগ করে। অর্থাৎ বাস্তবের বেশিরভাগ ব্যবস্থা দুই ধারণা মেশায়: subscription-গুলোর মধ্যে pub/sub, একটি subscription-এর ভেতরে competing consumers. Log-ভিত্তিক broker-এ একই ধারণার নাম consumer group (পর্ব ৩৩, log-ভিত্তিক streaming).

Point-to-point (queue)Pub/sub (topic)
একটি বার্তা কতজন প্রক্রিয়া করেএকজন কর্মীপ্রতিটি subscription একবার করে
প্রেরক কী জানেকোন queue-তে কাজ যাবেশুধু topic-এর নাম
বার্তার ধরনসাধারণত command: “এই কাজটি করুন”সাধারণত event: “এটি ঘটেছে”
নতুন গ্রাহক যোগএকই কাজের জন্য কর্মী বাড়েনতুন subscription, publisher অপরিবর্তিত
উদাহরণছবি resize-এর কাজ, PDF তৈরিTicketBooked → SMS, loyalty, analytics

Publisher → topic → subscription → সেবা

Two publishers, two topics, three subscriptions (push or pull), three independent services

Publisher, topic, subscription আর সেবা — pub/sub-এর কাঠামো Booking service Payment service topic: booking topic: payment sms-sub loyalty-sub analytics-sub SMS Loyalty Analytics booking.created … payment.failed … booking.* channel = app # (সব topic) push পায় pull করে pull · batch প্রতিটি subscription-এর নিজের কপি, নিজের গতি publisher কোনো subscriber-কে চেনে না একটি সেবা বন্ধ? শুধু তার subscription-এ কপি জমে push pull pull publish publish
ছবি ৩২.১ — কঠিন তীর: broker পাঠায় (push); ভাঙা তীর: সেবা নিজে টেনে নেয় (pull). Analytics দুই topic-এরই গ্রাহক; Payment service জানেও না যে analytics আছে।

পরিভাষা সংস্থাভেদে আলাদা: কোথাও “topic” আর “subscription” (Google Cloud Pub/Sub-এর মতো), কোথাও “exchange” আর “queue” (RabbitMQ), কোথাও SNS topic থেকে কয়েকটি SQS queue (AWS-এর প্রচলিত fan-out ধাঁচ), আর Kafka-য় topic আর consumer group. ধারণা একই: একটি প্রকাশ, কয়েকটি স্বাধীন গ্রাহক-কপি. আরেকটি জিনিস মনে রাখুন: পর্ব ১০-এর WebSocket backplane-এ যে Redis-ধাঁচের pub/sub দেখেছেন, তা বার্তা জমিয়ে রাখে না — subscriber সংযুক্ত না থাকলে বার্তা হারায়। এই পর্বের subscription টেকসই (durable) ধরে নেওয়া হয়েছে: সেবা বন্ধ থাকলেও কপি অপেক্ষা করে।

২ · Push বনাম pull, আর কে কোন বার্তা পাবে — filter

Push subscription

Push subscription-এ broker নিজে প্রতিটি বার্তা subscriber-এর একটি HTTP endpoint-এ পাঠায়; 2xx উত্তর মানে ack, অন্য কিছু বা timeout মানে পরে আবার চেষ্টা। সুবিধা: subscriber-কে সারাক্ষণ জিজ্ঞেস করতে হয় না, দেরি কম, serverless function-এর মতো “ডাকলে জাগে” সেবার জন্য সহজ। অসুবিধা: গতি ঠিক করে broker, তাই subscriber ধীর হলে broker-কে হার কমাতে হয়; আর endpoint-টি broker-এর নাগালে খোলা রেখে প্রতিটি অনুরোধ যাচাই করতে হয় (অংশ ৮-এর webhook-এর মতো)।

Pull subscription

Pull subscription-এ subscriber নিজে broker-কে বলে, “আমাকে সর্বোচ্চ ১০০টি দিন।” সুবিধা: গতি subscriber-এর হাতে — যতটা পারে ততটা টানে, তাই backpressure স্বাভাবিকভাবেই থাকে (পর্ব ১৮); batch-এ নিলে প্রতি বার্তার খরচ কমে। অসুবিধা: খালি সময়ে বারবার জিজ্ঞেস করার অপচয় — তাই long polling-এর মতো “কিছু না এলে কিছুক্ষণ ধরে রাখুন” কৌশল; আর batch ভরার অপেক্ষায় কিছু দেরি। Kafka-র consumer pull করে; Google Cloud Pub/Sub দুটোই দেয়।

PushPull
গতি কে ঠিক করেbroker (subscriber-এর উত্তর দেখে)subscriber নিজে
দেরিকম — বার্তা এলেই যায়batch আর অপেক্ষার সময় যোগ হয়
Request-এর সংখ্যাপ্রায় প্রতি বার্তায় একটিপ্রতি batch-এ একটি
Subscriber-এর দিকে কী লাগেখোলা, যাচাই করা HTTPS endpointbroker-এর সাথে বাইরে যাওয়ার সংযোগ, কোনো খোলা port নয়
মানানসইকম ভার, serverless, দ্রুত সাড়াবেশি ভার, batch-এ প্রক্রিয়া, নিজের গতিতে চলা

Filter — topic hierarchy আর attribute

সব subscriber সব event চায় না। দুটি প্রচলিত উপায়:

  • Topic hierarchy: topic-এর নাম ডট দিয়ে স্তরে ভাগ — booking.created, booking.cancelled, booking.seat.held; subscription একটি pattern দেয়। RabbitMQ-র topic exchange-এ * মানে ঠিক একটি শব্দ, # মানে শূন্য বা তার বেশি শব্দ; MQTT-তে একই কাজ + আর #, NATS-এ * আর > — চিহ্ন আর খুঁটিনাটি পণ্যভেদে আলাদা।
  • Attribute filter: event-এর সাথে কয়েকটি key-value attribute (যেমন channel=app, route=dhaka-rajshahi); subscription বলে, “শুধু channel=app হলে দিন।” Google Cloud Pub/Sub-এর subscription filter আর AWS SNS-এর filter policy এই ধাঁচের; কী কী শর্ত লেখা যায় (সমান, prefix, সংখ্যার পরিসর) আর কতগুলো — লেখার সময়ে পণ্যভেদে আলাদা, ডকুমেন্টেশন দেখে নিন।

Filter broker-এ চললে লাভ: যে বার্তা কেউ চায় না, তা network-এ যায়ই না। Subscriber নিজে ফেলে দিলে কাজ হয়, কিন্তু প্রতিটি বার্তার bandwidth আর প্রক্রিয়ার খরচ দিতে হয়। সাবধানতা: filter সাধারণত শুধু attribute বা topic-এর নাম দেখে, payload-এর গভীরে নয় — তাই কোন তথ্য attribute-এ যাবে, তা event নকশার অংশ।

নিচের simulator একটি ছোট in-memory broker. তিনটি subscription, প্রতিটির নিজের queue: sms চায় booking.*, loyalty চায় শুধু app থেকে আসা booking.created, analytics চায় সব (#). পাঁচটি event publish করে দেখা যাক কে কী পায়:

// In-memory pub/sub broker: topic patterns (* = one word, # = any words) + attribute filters.
function topicMatch(pattern, topic) {          // e.g. "booking.*" matches "booking.created"
  const p = pattern.split("."), t = topic.split(".");
  const go = (i, j) => {
    if (i === p.length) return j === t.length;
    if (p[i] === "#") return go(i + 1, j) || (j < t.length && go(i, j + 1));
    return j < t.length && (p[i] === "*" || p[i] === t[j]) && go(i + 1, j + 1);
  };
  return go(0, 0);
}
const subs = [                                   // each subscription has its OWN queue
  { name: "sms",       pattern: "booking.*",       filter: {} },
  { name: "loyalty",   pattern: "booking.created", filter: { channel: "app" } },
  { name: "analytics", pattern: "#",               filter: {} },
].map((s) => ({ ...s, queue: [] }));
function publish(topic, attrs, id) {
  const got = [];
  for (const s of subs) {
    const ok = topicMatch(s.pattern, topic) &&
      Object.entries(s.filter).every(([k, v]) => attrs[k] === v);
    if (ok) { s.queue.push(id); got.push(s.name); }
  }
  console.log(id.padEnd(5) + topic.padEnd(18) + JSON.stringify(attrs).padEnd(30) +
    "-> " + (got.join(", ") || "(nobody)"));
}
publish("booking.created",   { channel: "app" },          "e1");
publish("booking.created",   { channel: "web" },          "e2");
publish("booking.cancelled", { channel: "app" },          "e3");
publish("payment.failed",    { channel: "app" },          "e4");
publish("booking.seat.held", { channel: "app" },          "e5");
console.log("--- queues after 5 publishes ---");
for (const s of subs) console.log(s.name.padEnd(10) + s.queue.length + "  [" + s.queue.join(" ") + "]");
const total = subs.reduce((a, s) => a + s.queue.length, 0);
console.log("published 5, delivered " + total + " copies");

আউটপুট:

e1   booking.created   {"channel":"app"}             -> sms, loyalty, analytics
e2   booking.created   {"channel":"web"}             -> sms, analytics
e3   booking.cancelled {"channel":"app"}             -> sms, analytics
e4   payment.failed    {"channel":"app"}             -> analytics
e5   booking.seat.held {"channel":"app"}             -> analytics
--- queues after 5 publishes ---
sms       3  [e1 e2 e3]
loyalty   1  [e1]
analytics 5  [e1 e2 e3 e4 e5]
published 5, delivered 9 copies

e1 তিনজনই পেল; e2 web থেকে, তাই loyalty-র attribute filter তাকে বাদ দিল। e4 payment দিয়ে শুরু, তাই booking.*-এর সাথে মেলেনি। e5 লক্ষ করুন: booking.seat.held-এ তিনটি শব্দ, আর * ঠিক একটি শব্দ ধরে — তাই SMS এটি পায়নি; booking.# লিখলে পেত। ৫টি publish থেকে ৯টি কপি — fan-out. প্রতিটি subscription-এর নিজের queue, তাই একটির দৈর্ঘ্য অন্যটিকে প্রভাবিত করে না। একটি সত্যিকারের broker-এ এর সাথে থাকে টেকসই লেখা, ack, retry আর dead letter queue (পর্ব ৩১)।

৩ · Event-driven architecture — event, command আর query

Event-driven architecture (EDA) মানে এমন নকশা, যেখানে সেবাগুলো একে অপরকে সরাসরি ডাকার বদলে মূলত ঘটনার খবর প্রকাশ করে আর অন্যের খবর শুনে নিজের কাজ করে। এর আগে তিন ধরনের বার্তা আলাদা করে চেনা দরকার, কারণ গুলিয়ে ফেলাই বেশিরভাগ ভুলের শুরু:

📣 Event

অতীতে ঘটে গেছে এমন একটি সত্য: SeatHeld, PaymentCharged. প্রকাশক জানে না কে শুনবে, কেউ “না” বলতে পারে না — ঘটনা ঘটেই গেছে। একজন প্রকাশক, শূন্য বা বহু শ্রোতা।

past tense · pub/sub

📝 Command

একটি নির্দিষ্ট প্রাপকের কাছে অনুরোধ: HoldSeat, ChargePayment. প্রাপক রাজি না-ও হতে পারে (“সিট নেই”)। প্রেরক ফলাফল চায়।

imperative · point-to-point

❓ Query

অবস্থা জানতে চাওয়া, কিছু বদলায় না: GetSeatMap(trip 7). সাধারণত সরাসরি request-response — REST বা gRPC (পর্ব ৮ ও ৯)।

read-only · synchronous

একটি সহজ পরীক্ষা: বার্তাটি কি প্রাপককে কিছু করতে বলছে? তাহলে command, event-এর ছদ্মবেশে পাঠালেও। SendSmsRequested নামের “event” আসলে SMS সার্ভিসকে দেওয়া হুকুম — একে point-to-point queue-তে command হিসেবে পাঠানোই সৎ নকশা। Event-এ প্রকাশক শুধু নিজের জগতের সত্য জানায়; কে কী করবে, সেটি শ্রোতার ব্যাপার। Query সাধারণত বার্তায় যায় না; event থেকে বানানো স্থানীয় কপি দিয়েই অনেক সময় query-র দরকার কমানো যায় (নিচে)।

Event দিয়ে কী পাঠাবেন — Fowler-এর তিন ধরন

Martin Fowler ২০১৭ সালের একটি লেখায় (“What do you mean by ‘Event-Driven’?”) দেখান, “event-driven” বলতে মানুষ অন্তত চারটি আলাদা ধাঁচ বোঝান — event notification, event-carried state transfer, event sourcing আর CQRS; এখানে প্রথম তিনটি, কারণ নকশার সিদ্ধান্তে এরাই সরাসরি আসে। এই নামগুলো একটি প্রচলিত শ্রেণিবিভাগ, কোনো মানক নয়; বই আর দলভেদে সীমারেখা একটু আলাদা।

ধাঁচEvent-এ কী থাকেলাভদাম
Event notificationশুধু “কী হলো” আর id: {"booking":"B42","type":"TicketBooked"}ছোট event, প্রকাশকের কাঠামো লুকানো থাকেবিস্তারিত লাগলে শ্রোতা প্রকাশককে ডাকে — আবার সরাসরি নির্ভরতা আর ভার; একসাথে অনেকে ডাকলে ভিড়
Event-carried state transferশ্রোতার দরকারি অবস্থাও: রুট, সিট, দাম, যাত্রীর নামশ্রোতা নিজের স্থানীয় কপি রাখে, প্রকাশক বন্ধ থাকলেও চলে, query কমেবড় event, ডেটার বহু কপি, কপি পুরোনো হতে পারে; ব্যক্তিগত তথ্য ছড়ায়
Event sourcingপ্রতিটি পরিবর্তন event হিসেবে সংরক্ষিত; event-গুলোই আসল ডেটাপুরো ইতিহাস, অতীতের যেকোনো মুহূর্তের অবস্থা আবার বানানো যায়নকশা ও schema বদল কঠিন, আলাদা read model লাগে — পর্ব ৩৫ (event sourcing আর CQRS)

মাঝামাঝি পথও প্রচলিত: event-এ id আর শ্রোতাদের বেশিরভাগের যা লাগে (দাম, রুট), কিন্তু ব্যক্তিগত বা বড় তথ্য নয় — দরকার হলে শ্রোতা অনুমতিসহ আলাদা API-তে চায়। বাইরের প্রতিষ্ঠানে যাওয়া webhook-এ প্রায়ই হালকা event পাঠানো হয়, যাতে গ্রাহক সর্বশেষ অবস্থা নিজে এনে নেয় (অংশ ৮)।

মূল ধারণা

Event হলো প্রকাশকের জগতের একটি অতীত সত্য; command হলো অন্যের কাছে অনুরোধ। Event-এ কতটা তথ্য দেবেন, সেটি একটি trade-off: কম দিলে শ্রোতা আবার ফোন করে (coupling ফেরে), বেশি দিলে ডেটার কপি আর গোপনীয়তার দায় বাড়ে।

৪ · একটি ভালো event — নাম, id, সময়, envelope আর version

Event একবার প্রকাশ হলে তা অনেকের চুক্তি — কে কোথায় কোন field পড়ছে, প্রকাশক জানেও না। তাই event-কে একটি public API-র মতো যত্নে নকশা করুন।

  • অতীত কালে, ব্যবসার ভাষায় নাম: TicketBooked, PaymentRefunded — “কী ঘটেছে”। BookingTableRowUpdated নয় (ডেটাবেসের ভাষা), SendSms নয় (হুকুম)।
  • অনন্য event id: প্রতিটি event-এর একটি UUID বা সমতুল্য id. Broker at-least-once দিলে একই event দুবার আসবে; শ্রোতা এই id দেখেই দ্বিতীয় কপি চেনে (পর্ব ৩৪)।
  • ঘটনার সময় আর প্রকাশের সময় আলাদা: time — ঘটনা কখন ঘটেছে (UTC, ISO 8601). Event দেরিতে বা উল্টো ক্রমে আসতে পারে; ঘড়িও পুরোপুরি মেলে না (পর্ব ৩৯)।
  • উৎস আর subject: কে প্রকাশ করেছে (source), কোন entity নিয়ে (subject, যেমন booking/B42). একই entity-র event-এর ক্রম চাইলে এটিই ordering key বা partition key হয় (পর্ব ৩৩)।
  • Type আর schema version: type আর dataschema বা version; schema বদলানোর নিয়ম — নতুন field যোগ ঠিক আছে, পুরোনো field-এর অর্থ বদলানো নয় — পর্ব ১২-এর schema evolution. শ্রোতারা tolerant reader হোক: অচেনা field উপেক্ষা করুক।
  • সুতো: correlation id — একটি ব্যবসায়িক প্রবাহের সব বার্তায় একই (যেমন অর্ডার ৪২); causation id — কোন বার্তার ফলে এটি তৈরি হলো। এ দুটিই পরে debugging-এর ভরসা (অংশ ৭)।

এই সাধারণ field-গুলোকে বলে envelope: খামের ওপর প্রেরক, প্রাপক আর তারিখ, ভেতরে চিঠি। ডাকঘরের বাছাইকারী খাম পড়েই চিঠি ঠিক থলেতে দেন, চিঠি খোলেন না — broker আর filter-ও তেমনি envelope বা attribute দেখে। CNCF-এর CloudEvents specification (সংস্করণ ১.০, ২০১৯) এমন একটি মানক envelope দেয়, যার বাধ্যতামূলক field id, source, specversion আর type; বাকিগুলো ঐচ্ছিক বা extension. নিচে সেই ধাঁচে একটি event:

{
  "specversion": "1.0",
  "id": "0b7c4e9a-6d1f-4a37-9a7e-2f5c8d1e3b40",
  "source": "/booking-service",
  "type": "com.example.booking.ticket.booked.v2",
  "subject": "booking/B42",
  "time": "2026-10-28T14:05:09Z",
  "datacontenttype": "application/json",
  "correlationid": "order-42",
  "causationid": "evt-payment-charged-9f21",
  "data": {
    "bookingId": "B42",
    "trip": 7,
    "route": "dhaka-rajshahi",
    "seat": "A3",
    "amountPaisa": 85000,
    "channel": "app"
  }
}

লক্ষ করুন: টাকার পরিমাণ পয়সায় পূর্ণসংখ্যা (amountPaisa), দশমিক নয় (পর্ব ১২); যাত্রীর ফোন নম্বর event-এ নেই — যার দরকার (SMS সার্ভিস), সে অনুমতিসহ আলাদা জায়গা থেকে নেবে। correlationid আর causationid CloudEvents-এর মূল field নয়, extension হিসেবে যোগ করা — নামকরণ দলভেদে আলাদা।

Event প্রকাশের আগে ডেটাবেসে লেখা — dual write-এর ফাঁদ: db.commit()-এর পরে publish() — মাঝে crash হলে অর্ডার আছে, event নেই; উল্টো ক্রমে হলে ভূতুড়ে event. সমাধান transactional outbox, পর্ব ২৫-এ বিস্তারিত (dual write আর outbox); এখানে আবার বলা হচ্ছে না, কিন্তু প্রতিটি publisher-এর জন্য এটি প্রযোজ্য।

৫ · একটি event, অনেক সেবা — fan-out-এর পথ আর খরচ

এবার রুমানার একটি event কীভাবে তিন সেবায় পৌঁছায়, ধাপে ধাপে। SMS push subscription-এ, loyalty আর analytics pull-এ; analytics ঠিক তখন deploy-এর জন্য ৪০ সেকেন্ড বন্ধ, আর সেই সময়ে সেকেন্ডে প্রায় ৩০টি নতুন event আসছে (ধরা যাক)। “পরে” চাপুন:

  1. Bookingpublisher
  2. Brokertopic · subscription
  3. SMSpush
  4. Loyaltypull
  5. Analyticspull · batch
গোনা
e1-এর বাকি কপি
Booking কি কারও অপেক্ষায়?
ড্যাশবোর্ডে e1
  1. Publish. Booking (outbox-এর relay দিয়ে) e1 পাঠাল topic booking-এ; broker টেকসই লিখে ack দিল। Booking-এর দায় এখানেই শেষ — কে শুনবে, সে জানে না।
  2. মেলানো। Broker তিনটি subscription-এর filter মেলাল: booking.* ✓, channel = app ✓, # ✓ — তিনটি স্বাধীন কপি। Booking আর কারও অপেক্ষায় নেই।
  3. SMS — push. Broker নিজে SMS সার্ভিসের endpoint-এ POST করল; SMS গেল, 200 এল, sms-sub থেকে e1 মুছল। বাকি কপি ২।
  4. Loyalty — pull. Loyalty নিজের সময়ে pull করে e1 পেল, ৮৫ পয়েন্ট যোগ করে ack দিল। একই সময়ে analytics deploy-এর জন্য বন্ধ হলো।
  5. Analytics বন্ধ — শুধু তার কপি জমে। ৪০ সেকেন্ডে প্রায় ৪০ × ৩০ = ১,২০০টি নতুন event analytics-sub-এ জমল। SMS আর loyalty স্বাভাবিক; Booking কিছুই টের পায়নি। কিন্তু ড্যাশবোর্ড ৪০ সেকেন্ড পিছিয়ে — eventual consistency.
  6. ধরে ফেলা। Analytics ফিরে বড় batch-এ pull করে জমা কমাতে লাগল; e1-এর শেষ কপিও গোনা হলো। সরাসরি ডাকার নকশায় এই ৪০ সেকেন্ড Booking-এর ত্রুটি বা দেরি হতো; এখানে তা একটি সেবার সাময়িক পিছিয়ে থাকা।

Fan-out-এর হিসাব — একটি event × N subscriber

ধরা যাক publisher সেকেন্ডে E-টি event প্রকাশ করে, প্রতিটির আকার s, আর N-টি subscription সব event চায়। তাহলে:

  • Delivery: সেকেন্ডে E × N. Publish-এর খরচ N-এর ওপর নির্ভর করে না, delivery-রটা করে — তাই subscription বাড়লে চাপ পড়ে broker-এর বাইরে যাওয়ার পথে।
  • বাইরে যাওয়া bandwidth: E × N × s. ২,০০০ event/s, ১ KB, ১০টি subscription → ২০,০০০ × ১ KB = সেকেন্ডে ২০ MB.
  • জমা থাকা কপি: queue-per-subscription ধাঁচে প্রতিটি subscription-এর নিজের কপি — কেউ পিছিয়ে পড়লে N গুণ পর্যন্ত জায়গা। Log-ভিত্তিক broker একটি কপি রাখে, আর প্রতিটি গ্রাহক শুধু নিজের অবস্থান (offset) মনে রাখে (পর্ব ৩৩)।
  • প্রকাশকের availability: সরাসরি ডাকলে তিনটি ৯৯.৯% সেবার সবাইকে লাগলে ০.৯৯৯৩ ≈ ৯৯.৭০%; pub/sub-এ publish-এর জন্য শুধু broker লাগে, subscriber-দের availability আর গুণ হয় না। তবে প্রতিটি event-এর কোনো না কোনো subscriber-এ সমস্যার সম্ভাবনা ১ − pN: p = ০.৯৯৯, N = ১০ হলে প্রায় ১% (০.৯৯৫৫%) — সেকেন্ডে ২,০০০ event-এ প্রতি সেকেন্ডে প্রায় ২০টি event-এর কোথাও retry লাগবে। তাই প্রতিটি subscription-এর নিজের retry, DLQ আর monitoring চাই।

একটি event থেকে লাখো ব্যবহারকারীর কাছে push, SMS আর email পৌঁছানোর পুরো নকশা — যেখানে fan-out-এর এই হিসাব সবচেয়ে বড় ভূমিকা নেয় — পর্ব ৫৭-এর notification system case study-তে আসবে।

নিচের calculator push আর pull-এর request ও দেরিও দেখায়। সরল মডেল (সব মান ধরে নেওয়া): push প্রতি বার্তায় একটি request; pull একবারে সর্বোচ্চ ১০০টি বার্তা টানে, আর একটি pull শেষ হয় batch ভরলে বা ১ সেকেন্ড অপেক্ষার পর — Kafka-র মতো broker-এর “কমপক্ষে এত byte, সর্বোচ্চ এত অপেক্ষা” ধরনের setting-এর মতো। RTT ২ ms.

// Fan-out cost model: E events/s, N subscriptions, payload KB. All inputs are ASSUMED values.
const KB = 1, RTT = 2;                 // payload size (KB), broker-subscriber round trip (ms)
const BATCH = 100, WAIT = 1000;        // pull: up to BATCH msgs per call, long-poll waits at most WAIT ms
function row(E, N) {
  const deliveries = E * N;                          // every subscription gets its own copy
  const egressMBs = deliveries * KB / 1000;          // MB/s leaving the broker
  const pushReq = deliveries;                        // push: one HTTP request per message
  const callMs = Math.min(BATCH / E * 1000, WAIT);   // pull: a call ends when the batch fills or WAIT ends
  const pullReq = N * 1000 / callMs;                 // pull calls per second, all subscriptions
  const pushLat = RTT / 2;                           // push: message leaves as soon as it arrives
  const pullLat = callMs / 2 + RTT / 2;              // pull: on average waits half a call
  return [E, N, deliveries, egressMBs.toFixed(1), pushReq, pullReq.toFixed(0),
          pushLat.toFixed(0) + " ms", pullLat.toFixed(0) + " ms"];
}
const head = ["events/s", "subs", "deliv/s", "MB/s out", "push req/s", "pull req/s", "push +lat", "pull +lat"];
const show = (r) => console.log(r.map((x) => String(x).padStart(11)).join(""));
show(head);
for (const [E, N] of [[2000, 1], [2000, 3], [2000, 10], [2000, 50], [5, 10]]) show(row(E, N));
// storage: queue-per-subscription keeps N copies; a shared log keeps 1 copy + N offsets (see lesson 33)
const E = 2000, N = 10, hours = 24, perDayGB = E * KB * 3600 * hours / 1e6;
console.log("1-day backlog at " + E + " ev/s: shared log " + perDayGB.toFixed(1) + " GB, " +
  N + " queues " + (perDayGB * N).toFixed(1) + " GB");

আউটপুট:

   events/s       subs    deliv/s   MB/s out push req/s pull req/s  push +lat  pull +lat
       2000          1       2000        2.0       2000         20       1 ms      26 ms
       2000          3       6000        6.0       6000         60       1 ms      26 ms
       2000         10      20000       20.0      20000        200       1 ms      26 ms
       2000         50     100000      100.0     100000       1000       1 ms      26 ms
          5         10         50        0.1         50         10       1 ms     501 ms
1-day backlog at 2000 ev/s: shared log 172.8 GB, 10 queues 1728.0 GB

Delivery, bandwidth আর push request — তিনটিই N-এর সাথে সরলরেখায় বাড়ে: ১ থেকে ৫০ subscription-এ ২,০০০ থেকে ১,০০,০০০ delivery/s, ২ থেকে ১০০ MB/s. Pull-এ request ১০০ গুণ কম (প্রতি ১০০ বার্তায় একটি call; ২,০০০/s-এ batch ভরে ৫০ ms-এ), দাম গড়ে ২৫ ms বাড়তি দেরি — মোট ২৬ ms. শেষ সারিটি উল্টো দিক দেখায়: ভার কম হলে (৫ event/s) batch ভরে না, তাই প্রতিটি pull পুরো ১ সেকেন্ড অপেক্ষা করে আর গড় দেরি দাঁড়ায় ৫০১ ms — push তখনো ১ ms. শেষ লাইন: সবাই এক দিন পিছিয়ে থাকলে একটি shared log-এ ১৭২.৮ GB, আর দশটি আলাদা queue-তে ১,৭২৮ GB. মডেলটি শুধু প্রবণতা দেখায়, কোনো পণ্যের মাপ নয়।

৬ · Choreography বনাম orchestration — একই প্রবাহ, দুই নকশা

পর্ব ২৫-এ (saga) দুই ধাঁচের পরিচয় পেয়েছেন। এবার একটি প্রবাহ দুইভাবে চালিয়ে পাশাপাশি দেখা যাক। রুমানার অর্ডার ৪২: সিট hold → টাকা কাটা → অপারেটরের টিকিট; তৃতীয় ধাপে অপারেটর জানায় “ট্রিপ বাতিল” — তাই টাকা ফেরত আর সিট ছাড়া।

বিয়ের অনুষ্ঠানের দুই রকম আয়োজন। Choreography: কোনো ম্যানেজার নেই; বাবুর্চি জানেন “অতিথি এলে খাবার দিতে হবে”, ডেকোরেটর জানেন “বর এলে গেটের ফুল”, সবাই চারপাশে কী ঘটছে দেখে নিজের কাজ ধরেন, নাচের দলের মতো। Orchestration: একজন অনুষ্ঠান-ব্যবস্থাপক তালিকা ধরে একে একে বলেন “এখন খাবার”, “এখন ফুল”, আর সবার উত্তর খাতায় টুকে রাখেন — অর্কেস্ট্রার পরিচালকের মতো। প্রথমটিতে কেউ একা আটকে দেয় না, কিন্তু “অনুষ্ঠান এখন কোন পর্যায়ে?” কেউ একা বলতে পারেন না; দ্বিতীয়টিতে ব্যবস্থাপক সব জানেন, কিন্তু তিনি অসুস্থ হলে সবাই দাঁড়িয়ে থাকেন।

Choreography — কেন্দ্রহীন event-শিকল

Choreography-তে প্রতিটি সেবা জানে কোন event শুনে কী করবে আর তারপর কোন event প্রকাশ করবে। নিচে event bus-এর log আর প্রতিটি সেবার অবস্থা:

প্রতিটি সেবার নিজের অবস্থা
Order
Seat
Wallet
Ticket
কে জানে পুরো অবস্থা?
  1. OrderPlaced. Order সার্ভিস অর্ডার ৪২ PENDING লিখে event প্রকাশ করল। কে শুনবে, তা Order-এর কোডে লেখা নেই।
  2. Seat শুনল। Seat সার্ভিস OrderPlaced-এর subscriber; A3 hold করে SeatHeld প্রকাশ করল।
  3. Payment শুনল। SeatHeld দেখে ৮৫০ টাকা কাটল (wallet ৪,১৫০), PaymentCharged প্রকাশ করল। লক্ষ করুন: Payment-কে জানতে হচ্ছে Seat-এর event-এর নাম আর অর্থ।
  4. Ticket ব্যর্থ। অপারেটর জানাল ট্রিপ বাতিল; Ticket সার্ভিস TicketFailed প্রকাশ করল। এখন compensation শুরু হবে — কিন্তু কেউ “শুরু করছে” না, প্রত্যেকে শুধু শুনছে।
  5. Refund. Payment TicketFailed-এরও subscriber; টাকা ফেরত দিয়ে wallet আবার ৫,০০০, তারপর PaymentRefunded.
  6. সিট ছাড়া। Seat-কে এবার Payment-এর PaymentRefunded শুনতে হলো — Seat আর Payment পরস্পরের event চেনে। A3 free, SeatReleased প্রকাশ।
  7. শেষ — কিন্তু ছবি কোথায়? Order সার্ভিস SeatReleased শুনে ৪২ CANCELLED করল। মোট ৬টি event. “অর্ডার ৪২ কেন বাতিল?” — Order-এর কাছে শুধু শেষ event; কারণ খুঁজতে চারটি সেবার log correlation id দিয়ে জোড়া দিতে হবে।

Orchestration — একজন পরিচালক

Orchestration-এ একটি orchestrator (বা workflow engine, পর্ব ৩৭ — background job আর workflow engine) নিজের ডেটাবেসে প্রবাহের একটি সারি রাখে, প্রতিটি সেবাকে command পাঠায় আর উত্তর লিখে রাখে:

  1. Orchestratorsaga 42-এর সারি
  2. Seatশুধু command চেনে
  3. Paymentশুধু command চেনে
  4. Ticketশুধু command চেনে
গোনা
মোট বার্তা (command + reply)
“অর্ডার ৪২ কোথায়?”
  1. শুরু। Orchestrator saga ৪২-এর সারি STARTED লিখে Seat-কে HoldSeat command পাঠাল (বার্তা ১)।
  2. সিট হলো। Seat OK দিল (২), orchestrator সারিতে লিখল আর Payment-কে Charge পাঠাল (৩)। Seat জানে না পরে কী হবে — জানার দরকারও নেই।
  3. টাকা কাটা। Payment ৮৫০ কেটে OK দিল (৪); orchestrator Ticket-কে IssueTicket পাঠাল (৫)।
  4. টিকিট ব্যর্থ। Ticket FAILED জানাল (৬)। Orchestrator সারিতে COMPENSATING লিখে উল্টো ক্রমে প্রথমে Refund পাঠাল (৭)। সিদ্ধান্তটি এক জায়গায়, এক কোডে।
  5. Refund. Payment ফেরত দিল, wallet ৫,০০০, OK (৮); orchestrator ReleaseSeat পাঠাল (৯)।
  6. শেষ। Seat A3 ছেড়ে OK দিল (১০); saga ৪২ CANCELLED, কারণসহ। বার্তা choreography-র ৬-এর জায়গায় ১০টি, কিন্তু যেকোনো মুহূর্তে একটি সারি পড়েই বলা যায় অর্ডার কোথায়।

এবার দুই নকশা কোডে, একই ব্যর্থতাসহ। প্রতিটি অংশ কোন বার্তা দেখেছে, তা আলাদা করে গোনা হচ্ছে — কে কতটা জানে, তার মাপ:

// Order 42: HoldSeat -> Charge -> IssueTicket (FAILS: trip cancelled) -> compensate. Two styles.
const seen = {};                                  // component -> messages it has received
const recv = (who, msg) => (seen[who] = seen[who] || []).push(msg);
const log = (n, s) => console.log(String(n).padStart(2) + " " + s);
console.log("== CHOREOGRAPHY: services react to events, nobody is in charge");
const react = {                                   // event -> [subscriber, event it publishes next]
  OrderPlaced: ["seat", "SeatHeld"],         SeatHeld: ["payment", "PaymentCharged"],
  PaymentCharged: ["ticket", "TicketFailed"], TicketFailed: ["payment", "PaymentRefunded"],
  PaymentRefunded: ["seat", "SeatReleased"], SeatReleased: ["order", null],
};
let ev = "OrderPlaced", n = 0;
while (ev) {
  const [svc, next] = react[ev];
  recv(svc, ev); log(++n, ev.padEnd(16) + "-> " + svc + (next ? " publishes " + next : " marks CANCELLED"));
  ev = next;
}
for (const c of ["order", "seat", "payment", "ticket"]) console.log("   " + c.padEnd(8) + "saw: " + seen[c].join(", "));
for (const k in seen) delete seen[k];
console.log("== ORCHESTRATION: one orchestrator sends commands and records every reply");
const plan = [["seat", "HoldSeat", "ReleaseSeat"], ["payment", "Charge", "Refund"], ["ticket", "IssueTicket", null]];
const done = []; n = 0;
for (const [svc, cmd, undo] of plan) {
  const ok = cmd !== "IssueTicket";
  recv(svc, cmd); recv("orchestrator", svc + ":" + (ok ? "OK" : "FAILED"));
  log(++n, ("cmd " + cmd).padEnd(16) + "-> " + svc); log(++n, "reply " + (ok ? "OK" : "FAILED").padEnd(10) + "<- " + svc);
  if (!ok) { for (const [s, , u] of done.reverse()) {
      recv(s, u); recv("orchestrator", s + ":" + u + " OK");
      log(++n, ("cmd " + u).padEnd(16) + "-> " + s); log(++n, "reply OK".padEnd(16) + "<- " + s); }
    break; }
  done.push([svc, cmd, undo]);
}
for (const c of ["orchestrator", "seat", "payment", "ticket"]) console.log("   " + c.padEnd(13) + "saw: " + seen[c].join(", "));

আউটপুট:

== CHOREOGRAPHY: services react to events, nobody is in charge
 1 OrderPlaced     -> seat publishes SeatHeld
 2 SeatHeld        -> payment publishes PaymentCharged
 3 PaymentCharged  -> ticket publishes TicketFailed
 4 TicketFailed    -> payment publishes PaymentRefunded
 5 PaymentRefunded -> seat publishes SeatReleased
 6 SeatReleased    -> order marks CANCELLED
   order   saw: SeatReleased
   seat    saw: OrderPlaced, PaymentRefunded
   payment saw: SeatHeld, TicketFailed
   ticket  saw: PaymentCharged
== ORCHESTRATION: one orchestrator sends commands and records every reply
 1 cmd HoldSeat    -> seat
 2 reply OK        <- seat
 3 cmd Charge      -> payment
 4 reply OK        <- payment
 5 cmd IssueTicket -> ticket
 6 reply FAILED    <- ticket
 7 cmd Refund      -> payment
 8 reply OK        <- payment
 9 cmd ReleaseSeat -> seat
10 reply OK        <- seat
   orchestrator saw: seat:OK, payment:OK, ticket:FAILED, payment:Refund OK, seat:ReleaseSeat OK
   seat         saw: HoldSeat, ReleaseSeat
   payment      saw: Charge, Refund
   ticket       saw: IssueTicket

Choreography-তে ৬টি event, orchestration-এ ১০টি বার্তা (৫টি command, ৫টি reply). কিন্তু “saw” লাইনগুলো আসল পার্থক্য দেখায়। Choreography-তে order saw: SeatReleased — Order শুধু শেষ খবর পেয়েছে, টিকিট যে ব্যর্থ হয়েছিল তা জানেই না; কারণ জানতে হলে তাকে TicketFailed-এরও subscriber হতে হয়। আর seat saw: OrderPlaced, PaymentRefunded — Seat-কে Order আর Payment দুই সেবার event চিনতে হয়েছে: coupling দূর হয়নি, event-এর নামে সরে গেছে। Orchestration-এ orchestrator saw লাইনে পুরো ইতিহাস এক জায়গায়, আর প্রতিটি সেবা শুধু নিজের দুটি command চেনে — অন্য কোনো সেবার নামও জানে না।

পাশাপাশি তুলনা

ChoreographyOrchestration
পুরো প্রবাহের যুক্তি কোথায়সব সেবার subscription-এ ছড়ানো; কোনো একটি ফাইলে নেইorchestrator-এর এক জায়গায়, পড়া ও পরীক্ষা করা যায়
Visibility — “এখন কোথায়?”সব log correlation id দিয়ে জোড়া লাগে, বা আলাদা একটি tracking সেবাorchestrator-এর একটি সারি
Couplingসেবারা পরস্পরের event-এর নাম ও অর্থে বাঁধাorchestrator সবার command জানে; সেবারা একে অপরকে জানে না
নতুন শ্রোতা যোগ (যেমন loyalty)খুব সহজ — শুধু নতুন subscriptionসহজ, যদি সে শুধু শেষের event শোনে; প্রবাহে ঢোকাতে orchestrator বদলায়
প্রবাহের ক্রম বদলকয়েকটি সেবার subscription একসাথে বদলাতে হয় — ঝুঁকিপূর্ণএকটি সেবার কোড বদল
Timeout — “কেউ সাড়া দিচ্ছে না”কে টের পাবে? প্রায়ই কেউ না, যতক্ষণ না আলাদা নজরদারি থাকেorchestrator-এর timer
ব্যর্থতার কেন্দ্রনেই; প্রতিটি সেবা স্বাধীনorchestrator — তাই তার অবস্থা টেকসই, সে replicated
ঝুঁকিevent-এর চক্র, “কে কাকে শোনে” বোঝা কঠিনসব ব্যবসায়িক যুক্তি জমে মোটা “god service”

বাস্তবে প্রায়ই মিশ্র পথ: একটি দলের সীমানার ভেতরের জটিল প্রবাহ (টাকা আর compensation-সহ বুকিং) orchestration-এ, আর সীমানা পেরিয়ে অন্য দলকে খবর দেওয়া (TicketBooked → loyalty, analytics, notification) choreography-তে। নিয়ম মনে রাখুন: যে প্রবাহের একজন ব্যবসায়িক মালিক আছেন আর যার শেষ অবস্থা কাউকে জবাবদিহি করতে হয়, তার একটি স্পষ্ট মালিক-কোডও থাকুক। যে খবরের পরে কী হবে তা প্রকাশকের মাথাব্যথা নয়, তা event হয়ে ছড়াক।

৭ · Temporal decoupling-এর দাম — consistency, ক্রম, duplicate আর debugging

Pub/sub-এর বড় উপহার temporal decoupling: প্রকাশক আর শ্রোতাকে একই সময়ে চালু থাকতে হয় না। Analytics ৪০ সেকেন্ড বন্ধ থাকলেও বুকিং চলেছে (অংশ ৫)। কিন্তু সময় আলাদা হওয়া মানে কয়েকটি নতুন দায়:

  • Eventual consistency: রুমানা টিকিট কেনার পর loyalty পাতায় পয়েন্ট দেখলেন না — event এখনো প্রক্রিয়া হয়নি। UI-তে “হালনাগাদ হচ্ছে” দেখান, বা যে পাতা সঙ্গে সঙ্গে সঠিক হতেই হবে, তা সরাসরি উৎস থেকে পড়ুন (পর্ব ২২-এর read-your-writes). Event-ভিত্তিক কপি সবসময় “কিছুটা পুরোনো” হতে পারে — নকশায় এটি মেনে নিন, লুকাবেন না।
  • ক্রম: BookingCancelled যদি BookingCreated-এর আগে পৌঁছায়? সাধারণত শুধু একই key-র (একই বুকিং) event-এর ক্রম রক্ষা করা যায়, সব event-এর নয় — ordering key বা partition (পর্ব ৩৩)। শ্রোতা version বা sequence number দেখে পুরোনো event উপেক্ষা করতে পারে।
  • Duplicate: প্রায় সব নির্ভরযোগ্য broker at-least-once — ack হারালে আবার আসে। প্রতিটি শ্রোতা event id দিয়ে idempotent হোক; বিস্তারিত পর্ব ৩৪-এ (delivery semantics).
  • Debugging: সরাসরি ডাকলে একটি stack trace বা একটি HTTP log-এ পুরো পথ দেখা যায়; event-শিকলে কারণ আর ফল আলাদা সেবায়, আলাদা সময়ে। “Payment কেন refund করল?” — উত্তর Ticket-এর log-এ, তিন সেকেন্ড আগে।
  • নিঃশব্দ ব্যর্থতা: কোনো শ্রোতা ভুল filter দিয়ে কিছুই পাচ্ছে না, বা একটি subscription মুছে গেছে — প্রকাশক কখনো error দেখবে না। তাই প্রতিটি subscription-এর জমা (backlog), সবচেয়ে পুরোনো না-পড়া বার্তার বয়স আর DLQ-র আকার monitor করুন।

সুতো জোড়া — correlation id আর distributed tracing

উপায় হলো প্রতিটি বার্তায় সুতো রাখা। প্রতিটি event-এ correlation id (পুরো প্রবাহের এক id) আর causation id (কোন বার্তা থেকে জন্ম)। প্রতিটি সেবা log-এ এ দুটি লেখে, আর নিজে নতুন event প্রকাশ করলে correlation id হুবহু কপি করে, causation id-তে নিজের পাওয়া event-এর id বসায়। তাহলে একটি query-তে পুরো গাছ: OrderPlaced → SeatHeld → PaymentCharged → TicketFailed → …. Distributed tracing একই ধারণাকে যন্ত্রে বসায় — trace context (W3C Trace Context-এর traceparent header-এর মতো) বার্তার header বা attribute-এ বয়ে নিয়ে যাওয়া হয়; বিস্তারিত পর্ব ৫০-এ (observability). Asynchronous পথে এই context হাতে বসাতে হয়, অনেক লাইব্রেরি নিজে থেকে করে না — যাচাই করে নিন।

14:05:09.120 order   corr=order-42 id=e-101 cause=-     OrderPlaced published
14:05:09.180 seat    corr=order-42 id=e-102 cause=e-101 SeatHeld A3
14:05:09.410 payment corr=order-42 id=e-103 cause=e-102 PaymentCharged 850
14:05:12.730 ticket  corr=order-42 id=e-104 cause=e-103 TicketFailed: trip cancelled
14:05:12.900 payment corr=order-42 id=e-105 cause=e-104 PaymentRefunded 850
14:05:13.050 seat    corr=order-42 id=e-106 cause=e-105 SeatReleased A3

৮ · Webhook — প্রতিষ্ঠানের সীমানা পেরোনো pub/sub

আপনার বাস-টিকিট সেবা একটি অংশীদার ট্রাভেল এজেন্সিকে (কাল্পনিক) জানাতে চায়, কখন তাদের বিক্রি করা টিকিট বাতিল হলো। তারা আপনার broker-এ subscribe করতে পারে না — ভিন্ন প্রতিষ্ঠান, ভিন্ন network. সমাধান webhook: অংশীদার একটি HTTPS URL নিবন্ধন করে, আর ঘটনা ঘটলে আপনি সেখানে POST করেন। এটি আসলে push subscription, শুধু ইন্টারনেট পেরিয়ে, এমন একজন গ্রাহকের কাছে যার সার্ভার আপনার নিয়ন্ত্রণে নেই। নিরাপত্তা আর নির্ভরযোগ্যতার দায় দুই দিকেই।

প্রদাতা হিসেবে — যা পাঠাবেন

  • স্বাক্ষর: প্রতিটি অংশীদারের জন্য আলাদা গোপন চাবি; timestamp + "." + raw body-এর ওপর HMAC-SHA256 হিসাব করে header-এ পাঠান। গ্রাহক একই হিসাব করে মিলিয়ে নেয় — বার্তাটি আপনারই, পথে বদলায়নি। (HMAC-এর ভিত্তি Computer Networks কোর্সের cryptography পর্বে।)
  • Timestamp আর event id: স্বাক্ষরে timestamp থাকায় গ্রাহক পুরোনো বার্তা প্রত্যাখ্যান করতে পারে (যেমন ৫ মিনিটের বেশি পুরোনো) — পুরোনো বৈধ বার্তা আবার পাঠানোর (replay) বিরুদ্ধে সুরক্ষা; event id দিয়ে সে duplicate চেনে।
  • Retry, backoff আর শেষ সীমা: 2xx না পেলে exponential backoff আর jitter-সহ আবার পাঠান (পর্ব ৪৮) — কয়েক ঘণ্টা বা দিন পর্যন্ত; তারপর DLQ-তে রাখুন, অংশীদারকে জানান, আর বারবার ব্যর্থ endpoint সাময়িকভাবে বন্ধ করুন। প্রতিটি অংশীদারের আলাদা queue, যাতে একজনের ধীর সার্ভার অন্যদের আটকে না দেয়।
  • ক্রমের প্রতিশ্রুতি দেবেন না: retry-র কারণে পরের event আগে পৌঁছাতে পারে। হালকা event (“বুকিং B42 বদলেছে”) পাঠিয়ে গ্রাহককে API থেকে সর্বশেষ অবস্থা আনতে বলা প্রায়ই নিরাপদ।
  • চাবি বদল আর নিবন্ধিত URL যাচাই: চাবি বদলের সময় কিছুক্ষণ পুরোনো আর নতুন দুই চাবিতে স্বাক্ষর দিন। অংশীদারের দেওয়া URL-এ পাঠানোর সময় শুধু HTTPS, আর আপনার ভেতরের network-এর ঠিকানায় (private IP, metadata ঠিকানা) পাঠানো আটকান — নইলে webhook-ব্যবস্থাই আপনার ভেতরের সেবায় পৌঁছানোর পথ হয়ে যায়।

গ্রাহক হিসেবে — যা যাচাই করবেন

on POST /webhooks/bus-tickets:
    body   = raw request bytes            # verify BEFORE parsing JSON
    ts     = header "X-Timestamp"
    sig    = header "X-Signature"
    if abs(now - ts) > 5 minutes:         reject 400   # stale: possible replay
    expect = HMAC_SHA256(secret, ts + "." + body)
    if not constant_time_equal(expect, sig): reject 401
    event  = parse_json(body)
    in one local transaction:
        if inbox has event.id:             return 200   # duplicate, already handled
        insert event.id into inbox; enqueue event for processing
    return 200 quickly                    # heavy work happens later, from the queue

তিনটি জিনিস লক্ষ করুন। স্বাক্ষর যাচাই হয় কাঁচা byte-এর ওপর — JSON parse করে আবার বানালে space বা ক্রম বদলে স্বাক্ষর মেলে না। তুলনা হয় constant-time-এ। আর ভারী কাজ পরে, queue থেকে — দ্রুত 2xx না দিলে প্রদাতা timeout ধরে আবার পাঠাবে, তাতে duplicate বাড়ে। Header-এর নাম আর স্বাক্ষরের ঠিক বিন্যাস প্রদাতাভেদে আলাদা; তাদের ডকুমেন্টেশন অনুযায়ী মেলান।

৯ · Anti-pattern — যেখানে event-driven নকশা জট পাকায়

Event-driven নকশা শুরুতে হালকা লাগে: “শুধু একটি event প্রকাশ করুন, বাকিরা যা খুশি করুক।” কয়েক বছর পরে তিনটি চেনা anti-pattern দেখা দেয় (নামগুলো প্রচলিত কথ্য নাম, মানক পরিভাষা নয়):

🍲 Event soup

শত শত event type, কে কোনটি প্রকাশ করে আর কে শোনে তার কোনো তালিকা নেই; একটি event বদলালে কোথায় কী ভাঙবে কেউ জানে না। ঢাকার যানজটের মতো — প্রতিটি গাড়ি নিজের মতো ঠিক, পুরো শহর অচল। প্রতিকার: event catalog (কে মালিক, কোন schema, কোন শ্রোতা), schema registry, প্রতিটি topic-এর একজন মালিক দল।

দৃশ্যমানতার অভাব

💬 Chatty event

প্রতিটি ছোট বদলে একটি event — SeatFieldUpdated সেকেন্ডে কয়েকবার, অথবা পাতলা event যার পরেই প্রতিটি শ্রোতা প্রকাশককে API-তে ডাকে। Fan-out × callback = প্রকাশকের ওপর N গুণ ভার। প্রতিকার: ব্যবসায়িক অর্থবহ event (TicketBooked, field-বদল নয়), প্রয়োজনীয় তথ্য event-এ, ঘনঘন বদল একসাথে জুড়ে পাঠানো।

fan-out-এর গুণফল

🧷 শেয়ার করা payload-এর লুকানো coupling

প্রকাশক নিজের ডেটাবেসের পুরো সারি event-এ ঢেলে দেয়; শ্রোতারা status_code = 3 বা ভেতরের কলামের নামে নির্ভর করতে শুরু করে। এখন প্রকাশক নিজের টেবিলও বদলাতে পারে না — কোনো API চুক্তি না করেই চুক্তি হয়ে গেছে (Hyrum-এর সূত্র)। প্রতিকার: ভেতরের মডেল আর প্রকাশিত event-এর মডেল আলাদা, প্রকাশ্য schema, version, আর শ্রোতার সাথে চুক্তির পরীক্ষা।

coupling ফিরে আসে

আরও দুটি সতর্কতা। Event দিয়ে request-response: GetPriceRequested event পাঠিয়ে PriceReturned-এর অপেক্ষা — সরাসরি একটি API কলের চেয়ে ধীর, জটিল আর debugging-এ কঠিন; এখানে query-ই ঠিক। সব কিছু event-driven: যে কাজে সঙ্গে সঙ্গে উত্তর লাগে (“এই সিট এখন খালি?”), সেখানে synchronous কল সৎ নকশা। Event-driven একটি যন্ত্র, ধর্ম নয়।

আরও গভীরে: Eugster, Felber, Guerraoui আর Kermarrec-এর “The Many Faces of Publish/Subscribe” (ACM Computing Surveys, ২০০৩) pub/sub-এর তিন রকম decoupling আলাদা করে — স্থান (কেউ কাউকে চেনে না), সময় (একসাথে চালু থাকতে হয় না) আর synchronization (প্রকাশক অপেক্ষায় আটকে থাকে না) — আর topic-ভিত্তিক বনাম content-ভিত্তিক subscription-এর তুলনা করে। Hohpe আর Woolf-এর Enterprise Integration Patterns (২০০৩) এই পর্বের অনেক ধারণার নাম দিয়েছে: Publish-Subscribe Channel, Point-to-Point Channel, Competing Consumers, Correlation Identifier, Process Manager (orchestrator-এর পূর্বসূরি ধারণা)। আর content-based routing-এর গবেষণা — লাখো filter-এর সাথে প্রতিটি event দ্রুত মেলানো — বড় broker-এর filter ইঞ্জিনের ভিত্তি।

পরীক্ষা ও ইন্টারভিউতে — প্রায়ই আসে এমন প্রশ্ন আর এক লাইনের উত্তর:

  • Queue আর topic-এর পার্থক্য? Queue-তে একটি বার্তা একজন কর্মী পায়; topic-এ প্রতিটি subscription নিজের কপি পায়।
  • Event আর command-এর পার্থক্য? Event অতীতের সত্য, শ্রোতা অজানা, প্রত্যাখ্যান করা যায় না; command নির্দিষ্ট প্রাপকের কাছে অনুরোধ, প্রত্যাখ্যাত হতে পারে।
  • সেকেন্ডে ১,০০০ event, ৮টি subscription, ২ KB — বাইরে যাওয়া bandwidth? ১,০০০ × ৮ × ২ KB = সেকেন্ডে ১৬ MB.
  • Choreography কখন বিপজ্জনক? লম্বা প্রবাহ, জটিল compensation আর timeout-এ — পুরো অবস্থা কেউ জানে না; তখন orchestration.
  • Webhook কীভাবে নিরাপদ করবেন? HMAC স্বাক্ষর (timestamp-সহ), সময়সীমা, event id দিয়ে duplicate বাদ, HTTPS আর backoff-সহ retry.

১০ · AI-এর যুগে event-driven নকশা যাচাই

“আমার অ্যাপকে event-driven করো” বললে AI প্রায়ই একটি চকচকে ছবি দেয়: “Kafka-তে সব event, প্রতিটি সেবা microservice, Redis cache, CQRS আর event sourcing.” দশজন ব্যবহারকারীর একটি অভ্যন্তরীণ টুলেও। চেনা ভুলগুলো:

AI-এর ভুলকেমন দেখায়কীভাবে যাচাই করবেন
অতিরিক্ত নকশাদুটি সেবার জন্য broker, schema registry, event sourcingজিজ্ঞেস করুন: কয়জন শ্রোতা? সরাসরি কল বা একটি queue কি যথেষ্ট?
Event আর command গোলানোSendEmailEvent, বা event-এ উত্তরের অপেক্ষাপ্রতিটি বার্তায়: এটি কি কাউকে কিছু করতে বলছে?
Fan-out-এর হিসাবে ভুল“১,০০০ event/s, তাই broker-এ ১,০০০ msg/s” — subscription-এর গুণ বাদনিজে E × N × s হিসাব; calculator-এ চালান
Failure mode উপেক্ষাduplicate, ক্রম, শ্রোতা বন্ধ, DLQ-র কথা নেইপ্রতিটি শ্রোতায়: দুবার এলে? উল্টো ক্রমে এলে? এক দিন বন্ধ থাকলে?
Consistency গোলানো“event-driven হলেও সব সময় consistent”, ACID-এর C আর CAP-এর C এক করাকোন পাতা কতক্ষণ পুরোনো দেখাতে পারে, লিখে নিন (পর্ব ২৪, ৪০)
পণ্যের সীমা বা ফিচার বানানো“এই broker-এ যেকোনো field-এ filter চলে”, “ক্রম সবসময় রক্ষা হয়”অফিশিয়াল ডকুমেন্টেশন; ছোট একটি পরীক্ষা চালিয়ে দেখা
Webhook-এ নিরাপত্তা বাদস্বাক্ষর ছাড়া POST, বা parse করার পরে যাচাইকাঁচা body-তে HMAC, timestamp, constant-time তুলনা আছে কি না

AI-কে নকশাকার নয়, কড়া reviewer বানান। আগে নিজে event-এর তালিকা আর প্রবাহের খসড়া করুন, তারপর এভাবে যাচাই করান:

তুমি একজন অভিজ্ঞ distributed systems reviewer. নিচে একটি বাস-টিকিট সেবার event-এর তালিকা, প্রতিটির প্রকাশক, শ্রোতা আর আনুমানিক হার (সেকেন্ডে কয়টি) দিলাম। [এখানে আপনার event, topic, subscription আর হারের খসড়া পেস্ট করুন] ১. কোনগুলো আসলে command বা query, event নয় — চিহ্নিত করো আর কারণ বলো। ২. প্রতিটি topic-এর fan-out হিসাব করো: delivery/s আর বাইরে যাওয়া bandwidth, ধাপে ধাপে; আমার সংখ্যা ব্যবহার করো, নিজে কিছু ধরে নিলে স্পষ্ট লেখো। ৩. প্রতিটি শ্রোতা দুবার বা উল্টো ক্রমে event পেলে, বা এক দিন বন্ধ থাকলে কী হবে — একটি টেবিলে দেখাও। ৪. প্রবাহটি choreography না orchestration-এ ভালো, আর কেন — visibility, coupling আর timeout ধরে বলো। ৫. Event-এর payload-এ কোথায় লুকানো coupling বা ব্যক্তিগত তথ্য ছড়ানোর ঝুঁকি আছে, দেখাও। ৬. কোনো broker-এর সীমা বা ফিচার বললে উৎস দাও, নিশ্চিত না হলে স্পষ্ট বলো; নতুন প্রযুক্তি যোগ করার আগে সরলতর বিকল্পটি বলো।

AI-এর নকশা মানার আগে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Pub/sub-এর ধারণা কোথা থেকে এল, আর “decoupling” কথাটি আসলে কত রকম?

“কেউ বলে, যারা আগ্রহী তারা শোনে” — ধারণাটি সফটওয়্যারের চেয়ে পুরোনো: সংবাদ সংস্থার তার-সেবা, শেয়ারবাজারের দামের টিকার। সফটওয়্যারে এর একটি প্রভাবশালী রূপ ১৯৯৩ সালের SOSP-এ প্রকাশিত “The Information Bus” গবেষণাপত্র (Oki, Pfluegl, Siegel আর Skeen). লেখকদের লক্ষ্য ছিল আর্থিক প্রতিষ্ঠানের ট্রেডিং ফ্লোরের সিস্টেম: নতুন অ্যাপ্লিকেশন চলমান সিস্টেম না থামিয়ে যোগ করা, আর প্রকাশককে না বদলে নতুন শ্রোতা বসানো। এর লেখকদের একজন Dale Skeen — যাঁকে পর্ব ২৫-এ three-phase commit-এর প্রসঙ্গে দেখেছেন। এই ধারার কাজ পরে বাণিজ্যিক “message bus” পণ্যে রূপ নেয়; আজকের cloud pub/sub আর Kafka-র মতো log-ভিত্তিক ব্যবস্থা একই প্রয়োজনের নতুন উত্তর।

২০০৩ সালে Eugster ও সহকর্মীরা “The Many Faces of Publish/Subscribe” জরিপে decoupling-কে তিন ভাগে ভাগ করেন, আর এই ভাগ নকশায় এখনো কাজে লাগে:

  • স্থানের decoupling: প্রকাশক আর শ্রোতা পরস্পরের ঠিকানা বা সংখ্যা জানে না — নতুন fraud-check সেবা যোগ করতে Booking বদলায় না।
  • সময়ের decoupling: দুজনকে একসাথে চালু থাকতে হয় না — analytics ৪০ সেকেন্ড বন্ধ থাকলেও বুকিং চলে। এর জন্য টেকসই subscription লাগে; স্মৃতিহীন pub/sub (যেমন পর্ব ১০-এর backplane) এটি দেয় না।
  • Synchronization-এর decoupling: প্রকাশক publish করে নিজের কাজে ফেরে, কারও কাজ শেষ হওয়ার অপেক্ষায় আটকে থাকে না।

এই ভাগ একটি বিভ্রান্তি কাটায়: মাঝে broker থাকলেই সব decoupling আসে না। “Event” পাঠিয়ে উত্তরের অপেক্ষায় বসে থাকলে synchronization-এর decoupling হারায়; webhook-এ URL জানতে হয়, তাই স্থানের decoupling আংশিক। আর কোনো decoupling-ই অর্থের coupling দূর করে না — শ্রোতাকে এখনো event-এর অর্থ আর schema বুঝতে হয়।

মূল উপলব্ধি: “loosely coupled” একটি মাত্রা নয়, কয়েকটি। নকশার সময় জিজ্ঞেস করুন — কে কাকে চেনে, কাকে একসাথে চালু থাকতে হয়, কে কার অপেক্ষায় থাকে, আর কে কার ডেটার অর্থে বাঁধা।

প্র ০২Event-এ শুধু id পাঠাবেন, নাকি পুরো ডেটা? Event notification বনাম event-carried state transfer-এর সিদ্ধান্ত কীভাবে নেবেন?

Martin Fowler-এর ২০১৭ সালের শ্রেণিবিভাগে দুটি প্রান্ত। Event notification-এ event পাতলা: “বুকিং B42 বাতিল হয়েছে।” শ্রোতার বিস্তারিত লাগলে সে Booking-এর API ডাকে। লাভ: event ছোট, প্রকাশকের ভেতরের মডেল লুকানো, আর শ্রোতা সবসময় সর্বশেষ অবস্থা পায় — উল্টো ক্রমে আসা event-এর সমস্যা অনেকটা কমে। দাম: প্রতিটি event-এর পর N জন শ্রোতা একসাথে API ডাকে। ঈদের রাতে সেকেন্ডে ২,০০০ event আর ১০ জন শ্রোতা মানে Booking-এর ওপর বাড়তি ২০,০০০ query/s, আর Booking বন্ধ থাকলে শ্রোতারাও কার্যত অচল — temporal decoupling আংশিক হারায়।

Event-carried state transfer-এ event-এ শ্রোতার দরকারি ডেটা থাকে; শ্রোতা নিজের একটি স্থানীয় কপি বা read model রাখে। Booking বন্ধ থাকলেও loyalty চলে, query-র ভার শূন্য। দাম:

  • আকার: event বড়, আর fan-out-এ সেই আকার N দিয়ে গুণ হয়।
  • পুরোনো কপি: শ্রোতার কপি সবসময় কিছুটা পিছিয়ে থাকে, আর উল্টো ক্রমে event এলে version দেখে মেলাতে হয়।
  • Payload-এর চুক্তি: event-এর প্রতিটি field একটি চুক্তি, তাই schema বদলানো কঠিন হয়।
  • ব্যক্তিগত তথ্য: সবচেয়ে বড় ঝুঁকি। যাত্রীর নাম, ফোন, NID event-এ থাকলে তা প্রতিটি শ্রোতার ডেটাবেস আর broker-এর retention-এ কপি হয়। মোছার অনুরোধ এলে সব কপি খুঁজে মোছা প্রায় অসম্ভব হয়ে পড়ে।

বাস্তবের সিদ্ধান্ত প্রায়ই মাঝামাঝি: বেশিরভাগ শ্রোতা যে কয়েকটি field চায়, সেগুলো event-এ; সংবেদনশীল ডেটা শুধু id দিয়ে, বিস্তারিত অনুমতি-যাচাই করা API-তে; বাইরের প্রতিষ্ঠানের জন্য পাতলা event আর “সর্বশেষ অবস্থা আনুন” API. আর event sourcing একেবারে আলাদা সিদ্ধান্ত: সেখানে event-ই সত্যের উৎস (পর্ব ৩৫)।

মূল উপলব্ধি: event-এর মোটা-পাতলা হওয়া আসলে ঠিক করে, coupling কোথায় থাকবে — API কলের মধ্যে, নাকি payload-এর চুক্তিতে। আর ব্যক্তিগত তথ্য এই হিসাবে সবচেয়ে ভারী ওজন।

প্র ০৩Choreography দিয়ে শুরু হওয়া একটি সিস্টেম কখন orchestration-এ সরানো উচিত, আর সেই সরানো নিরাপদে কীভাবে হয়?

অনেক দল choreography দিয়ে শুরু করে, কারণ শুরুতে এটি সবচেয়ে কম কাজ: দুটি সেবা, একটি event, কোনো নতুন উপাদান নেই। কয়েক বছর পরে চেনা লক্ষণগুলো দেখা দেয়:

  • “অর্ডার ৪২ কোথায় আটকে আছে?” — এই প্রশ্নের উত্তর দিতে চার দলের log খুঁজতে হয়।
  • নিঃশব্দে থেমে যাওয়া: একটি সেবা event হারালে প্রবাহ চুপচাপ থেমে যায়, আর কারও timer তা টের পায় না।
  • ক্রম বদলের ঝুঁকি: একটি নতুন ধাপ ঢোকাতে তিনটি সেবার subscription একসাথে বদলাতে হয়, আর deploy-এর ক্রম ভুল হলে প্রবাহ ভাঙে।
  • ছড়ানো compensation: উল্টো পথের যুক্তি কয়েকটি সেবায় ছড়ানো, ফলে কেউ পুরোটা পরীক্ষা করতে পারে না।

এগুলো বলে, প্রবাহটির নিজস্ব অবস্থা আছে, আর সেই অবস্থার একজন মালিক দরকার। Hohpe আর Woolf ২০০৩ সালেই এর নাম দেন Process Manager; আজ একই কাজ করে orchestrator বা workflow engine (open-source উদাহরণ Temporal), যা অবস্থা, timer আর retry টেকসই রাখে (পর্ব ৩৭)।

সরানোর নিরাপদ পথ সাধারণত ধাপে ধাপে। প্রথমে শুধু পর্যবেক্ষণ: একটি tracking সেবা সব event শোনে আর correlation id ধরে প্রতিটি অর্ডারের একটি অবস্থা-সারি বানায়, কিছুই নির্দেশ দেয় না। এতেই visibility আসে, আর বোঝা যায় কোন প্রবাহ কোথায় আটকায়। তারপর একটি নতুন প্রবাহ, বা একটি নতুন সংস্করণ, orchestrator দিয়ে চালু করা হয়; পুরোনো অর্ডার পুরোনো পথে শেষ হয় (strangler-এর ধাঁচ)। সেবাগুলো তখন event শোনার বদলে command নেয়, কিন্তু নিজের ঘটনার event প্রকাশ করতেই থাকে, যাতে loyalty বা analytics-এর মতো বাইরের শ্রোতারা কিছু টের না পায়।

উল্টো দিকটিও সত্য। Orchestrator-এ সব ব্যবসায়িক নিয়ম ঢুকে পড়লে সেটি একটি “god service” হয়ে ওঠে। নিয়মটি হলো: orchestrator ঠিক করে কখন কোন ধাপ চলবে, আর প্রতিটি সেবা ঠিক করে কীভাবে নিজের কাজ হবে।

মূল উপলব্ধি: প্রবাহের যদি একটি অবস্থা থাকে যা কাউকে জবাবদিহি করতে হয়, তাহলে সেই অবস্থার একটি ঠিকানাও থাকা দরকার। সেই ঠিকানা না থাকলে choreography ধীরে ধীরে একটি অদৃশ্য orchestrator-এ পরিণত হয়, যা সবার মাথায় ছড়িয়ে থাকে।

প্র ০৪“Event-driven মানেই loosely coupled” — এই দাবি কতটা সত্য? লুকানো coupling কোথায় থাকে?

Pub/sub স্থান আর সময়ের coupling কমায় — কেউ কারও ঠিকানা জানে না, কাউকে একসাথে চালু থাকতে হয় না। কিন্তু আরও কয়েক ধরনের coupling থেকে যায়, আর সেগুলো আগের চেয়ে কম দৃশ্যমান হয়:

  • Schema-র coupling: শ্রোতা amountPaisa পড়ে। প্রকাশক তা amount করলে শ্রোতা ভাঙে, অথচ প্রকাশকের কোডে কোনো compile error হয় না, কোনো কল ব্যর্থ হয় না। সমস্যা ধরা পড়ে উৎপাদনে, ঘণ্টাখানেক পরে।
  • অর্থের coupling: TicketBooked কি টাকা কাটার আগে প্রকাশ হয়, না পরে? Loyalty ধরে নিয়েছে পরে। প্রকাশক ক্রম বদলালে পয়েন্ট ভুল মানুষ পাবেন, অথচ schema একই আছে।
  • ক্রম আর সময়ের অনুমান: শ্রোতা ধরে নিয়েছে Created সবসময় Cancelled-এর আগে আসে, বা সব event ৫ সেকেন্ডের মধ্যে আসে।
  • Hyrum-এর সূত্র: যথেষ্ট ব্যবহারকারী থাকলে আপনার প্রতিটি দৃশ্যমান আচরণ কারও না কারও নির্ভরতা হয়ে যায় — event-এ ভুলে যাওয়া একটি debug field-ও।

সরাসরি API কলে কে কাকে ডাকে, তা অন্তত log-এ দেখা যায়; event-এ শ্রোতার তালিকা প্রকাশকের চোখের আড়ালে। তাই প্রতিকারগুলো দৃশ্যমানতা ফেরানোর:

  • প্রকাশ্য schema আর registry: registry-তে compatibility-র নিয়ম চালু থাকে, ফলে backward-incompatible বদল প্রকাশের আগেই আটকায় (পর্ব ১২)।
  • Event catalog: প্রতিটি event-এর মালিক, অর্থ আর পরিচিত শ্রোতাদের তালিকা।
  • চুক্তির পরীক্ষা: শ্রোতারা নিজেদের প্রত্যাশা পরীক্ষা হিসেবে লিখে দেয়, আর প্রকাশকের CI সেগুলো চালায় (consumer-driven contract).
  • আলাদা মডেল: ভেতরের ডেটাবেসের মডেল আর প্রকাশিত event-এর মডেল আলাদা রাখা হয়, যাতে টেবিল বদলালে চুক্তি না বদলায়।

বড় বদলে নতুন version কিছুদিন পুরোনোটির পাশাপাশি চলে — পর্ব ১২-এর expand-contract, এবার event-এ।

মূল উপলব্ধি: event-driven নকশা coupling দূর করে না, তার রূপ বদলায় — কলের তালিকা থেকে ডেটার চুক্তিতে। চুক্তিটি দৃশ্যমান আর পরীক্ষিত না হলে loosely coupled সিস্টেম আসলে হয় “লুকিয়ে coupled”.

প্র ০৫একজন webhook প্রদাতা হিসেবে আপনি কীভাবে নির্ভরযোগ্য আর নিরাপদ delivery দেবেন, যখন গ্রাহকের সার্ভার আপনার নিয়ন্ত্রণে নেই?

Webhook দেখতে সরল — ঘটনা ঘটলে একটি POST. আসলে এটি ইন্টারনেট পেরোনো push subscription, যার গ্রাহক ধীর, বন্ধ বা ভুল কনফিগার করা হতে পারে; তাই প্রদাতার নকশা একটি পূর্ণ delivery-ব্যবস্থা।

নির্ভরযোগ্যতা। ঘটনাটি প্রথমে outbox বা একটি টেকসই queue-তে যায় (পর্ব ২৫), তারপর delivery worker পাঠায়। ফলে প্রদাতার নিজের crash-এও event হারায় না। তারপর backoff-সহ কয়েক দিন পর্যন্ত retry, প্রতিটি গ্রাহকের আলাদা queue (একজনের ধীর সার্ভার যেন অন্যদের আটকে না দেয়), বারবার ব্যর্থ endpoint-এ বিরতি, আর সৎ প্রতিশ্রুতি: at-least-once, ক্রমের নিশ্চয়তা নেই — তাই স্থায়ী event id আর সর্বশেষ অবস্থা আনার একটি API.

নিরাপত্তা, প্রতিরক্ষার দিক থেকে। গ্রাহকের যাচাইয়ের জন্য: প্রতিটি গ্রাহকের আলাদা চাবি, timestamp আর কাঁচা body-র ওপর HMAC-SHA256, আর চাবি বদলের সময় কিছুদিন দুটি স্বাক্ষর। প্রদাতার নিজের দিকে সবচেয়ে উপেক্ষিত ঝুঁকি হলো গ্রাহকের দেওয়া URL. সেটি যদি আপনার ভেতরের network-এর কোনো ঠিকানা হয়, তাহলে আপনার delivery worker নিজেই ভেতরের সেবায় অনুরোধ পাঠাবে। প্রতিরক্ষা:

  • শুধু HTTPS.
  • DNS resolve করার পরে private, loopback আর cloud metadata ঠিকানা আটকানো — নিবন্ধনের সময়ে আর প্রতিটি delivery-র সময়ে।
  • delivery worker-কে একটি আলাদা, সীমিত network অংশে রাখা।
  • redirect অনুসরণ না করা, বা সীমিতভাবে করা।

শেষে আকার আর সময়ের সীমা: বড় payload-এর বদলে id আর API, আর প্রতিটি POST-এর ছোট timeout. Standard Webhooks-এর মতো কিছু open specification এই নিয়মগুলোকে একটি সাধারণ header বিন্যাসে বাঁধার চেষ্টা করছে। তবে লেখার সময়ে প্রদাতাভেদে বিন্যাস আলাদা।

মূল উপলব্ধি: webhook = outbox + retry + স্বাক্ষর + সীমা। গ্রাহককে যতটা সম্ভব কম বিশ্বাস করুন, আর তাকে এমন যন্ত্র দিন যাতে সে আপনাকে যাচাই করতে পারে।

অনুশীলন

আগে কাগজে নিজে করুন, তারপর উত্তর মিলিয়ে নিন।

  1. Fan-out-এর হিসাব: একটি অনলাইন বাজার সেবায় (কাল্পনিক) OrderPlaced event-এর হার পিকে সেকেন্ডে ৩০০, সারা দিনের গড় সেকেন্ডে ১২০; প্রতিটি event ২ KB; ৮টি subscription সব event চায়। বের করুন: (ক) পিকে delivery/s আর বাইরে যাওয়া bandwidth; (খ) দিনে মোট event আর একটি কপির হিসাবে দিনের ডেটা; (গ) ৩ দিনের retention-এ shared log-এ কত জায়গা, আর ৮টি আলাদা queue-তে সবাই পুরো ৩ দিন পিছিয়ে থাকলে কত; (ঘ) pull-এ batch ৫০, অপেক্ষা সর্বোচ্চ ১ সেকেন্ড — পিকে মোট pull call/s আর গড় বাড়তি অপেক্ষা; (ঙ) একটি subscriber ২ ঘণ্টা বন্ধ থাকল (গড় হারে); সে ফিরে সেকেন্ডে ৪০০টি প্রক্রিয়া করতে পারলে জমা শেষ করতে কত সময়?
    • (ক) ৩০০ × ৮ = ২,৪০০ delivery/s; ২,৪০০ × ২ KB = ৪,৮০০ KB/s = সেকেন্ডে ৪.৮ MB.
    • (খ) ১২০ × ৮৬,৪০০ = ১,০৩,৬৮,০০০টি event (প্রায় ১ কোটি); × ২ KB = ২,০৭,৩৬,০০০ KB ≈ ২০.৭ GB/দিন (১ GB = ১০৬ KB ধরে)।
    • (গ) shared log: ৩ × ২০.৭৩৬ ≈ ৬২.২ GB; ৮টি queue সবাই পুরো ৩ দিন পিছিয়ে থাকলে ৮ × ৬২.২০৮ ≈ ৪৯৭.৭ GB. স্বাভাবিক অবস্থায় queue-তে ack-এর পর কপি মোছে, তাই আসল ব্যবহার অনেক কম — কিন্তু নকশায় সবচেয়ে খারাপ অবস্থার জায়গা ভাবতে হয়।
    • (ঘ) পিকে ৫০টি বার্তা জমতে ৫০ ÷ ৩০০ সেকেন্ড ≈ ১৬৬.৭ ms (১ সেকেন্ডের কম, তাই batch ভরেই ফেরে)। প্রতি subscription সেকেন্ডে ৬টি call, মোট ৮ × ৬ = ৪৮ call/s (push হলে ২,৪০০ request/s). গড় বাড়তি অপেক্ষা ≈ ১৬৬.৭ ÷ ২ ≈ ৮৩ ms.
    • (ঙ) জমা: ১২০ × ৭,২০০ = ৮,৬৪,০০০টি (≈ ১.৭ GB). ফেরার পরেও নতুন event আসছে সেকেন্ডে ১২০, তাই জমা কমে সেকেন্ডে ৪০০ − ১২০ = ২৮০; সময় ৮,৬৪,০০০ ÷ ২৮০ ≈ ৩,০৮৬ সেকেন্ড ≈ ৫১ মিনিট. শিক্ষা: subscriber-এর ক্ষমতা গড় হারের চেয়ে যথেষ্ট বেশি না হলে একটি ছোট বিভ্রাট থেকে ফিরতে অনেক সময় লাগে।
  2. নকশার সমালোচনা: একটি দলের প্রস্তাব (কাল্পনিক) — “Order service প্রতিটি বদলে OrderUpdated প্রকাশ করবে, তাতে orders টেবিলের পুরো সারি (৪০টি কলাম, ডেটাবেসের নামে, গ্রাহকের ফোন নম্বরসহ) থাকবে। ১২টি সেবা subscribe করবে। Loyalty প্রতিটি event পেয়ে GET /orders/{id} ডেকে বিস্তারিত আনবে। Email সেবা status_code = 3 দেখলে রসিদ পাঠাবে। Event-এ কোনো id নেই; db.commit()-এর পরে producer.send(). অংশীদার এজেন্সিকে একই event স্বাক্ষর ছাড়া webhook-এ পাঠানো হবে।” অন্তত ছয়টি সমস্যা চিহ্নিত করে প্রতিটির সংশোধন প্রস্তাব করুন।
    সমস্যাকেন ক্ষতিকরসংশোধন
    একটি সাধারণ OrderUpdatedকী ঘটেছে বোঝা যায় না; প্রতিটি শ্রোতা দুটি সারি তুলনা করে অর্থ খোঁজে — chatty, অর্থহীনব্যবসায়িক event: OrderPlaced, OrderPaid, OrderCancelled
    পুরো সারি, ডেটাবেসের কলামের নামেলুকানো coupling — টেবিল বদলানো অসম্ভব; ১২টি সেবা ভেতরের মডেলে বাঁধাআলাদা প্রকাশিত schema, version, registry-তে compatibility যাচাই
    ফোন নম্বর event-এব্যক্তিগত তথ্য ১২টি ডেটাবেস আর broker-এর retention-এ ছড়ায়শুধু customer id; যার দরকার, অনুমতিসহ API থেকে
    Loyalty প্রতিটি event-এ GET ডাকেevent notification-এর দাম — Order-এর ওপর বাড়তি ভার, Order বন্ধ হলে loyalty-ও অচলloyalty-র দরকারি field (মোট দাম, customer id) event-এ — event-carried state transfer
    status_code = 3-এ নির্ভরতাঅর্থের coupling; কেউ কোড-তালিকা বদলালে রসিদ বন্ধ বা ভুলনির্দিষ্ট event OrderPaid শুনে রসিদ
    Event id নেইat-least-once-এ duplicate চেনা যায় না — দুটি রসিদ, দুবার পয়েন্টপ্রতিটি event-এ UUID; শ্রোতারা inbox দিয়ে idempotent (পর্ব ৩৪)
    commit-এর পরে senddual write — মাঝে crash হলে event হারায়transactional outbox (পর্ব ২৫)
    স্বাক্ষরহীন webhook, ভেতরের event হুবহুঅংশীদার যাচাই করতে পারে না; ভেতরের schema বাইরের চুক্তি হয়ে যায়HMAC স্বাক্ষর, timestamp, event id; বাইরের জন্য আলাদা, পাতলা event আর “সর্বশেষ অবস্থা” API

    এর সাথে: correlation id, প্রতিটি subscription-এর backlog আর DLQ monitoring, আর ১২টি শ্রোতার একটি event catalog — কে কোন event কেন শোনে।

  3. আউটপুট অনুমান করুন: অংশ ২-এর pubsub_broker.js-এ দুটি বদল: sms-এর pattern "booking.*" থেকে "booking.#", আর loyalty-র filter { channel: "app" } থেকে { channel: "web" }. চালানোর আগে লিখুন, প্রতিটি publish লাইনে কে পাবে, শেষে তিনটি queue-তে কী থাকবে আর মোট কয়টি কপি। তারপর simulator-এ বদলে মিলিয়ে দেখুন।
    e1   booking.created   {"channel":"app"}             -> sms, analytics
    e2   booking.created   {"channel":"web"}             -> sms, loyalty, analytics
    e3   booking.cancelled {"channel":"app"}             -> sms, analytics
    e4   payment.failed    {"channel":"app"}             -> analytics
    e5   booking.seat.held {"channel":"app"}             -> sms, analytics
    --- queues after 5 publishes ---
    sms       4  [e1 e2 e3 e5]
    loyalty   1  [e2]
    analytics 5  [e1 e2 e3 e4 e5]
    published 5, delivered 10 copies

    # শূন্য বা তার বেশি শব্দ ধরে, তাই booking.# এবার তিন শব্দের booking.seat.held-ও ধরল — SMS-এর queue ৩ থেকে ৪. Loyalty-র filter উল্টে যাওয়ায় সে এখন শুধু e2 পায় (web থেকে আসা booking.created), e1 নয়। Analytics অপরিবর্তিত। মোট কপি ৯ থেকে ১০. শিক্ষা: একটি wildcard চিহ্নের পার্থক্য কোন subscriber কী পাবে তা বদলে দেয় — আর প্রকাশক এর কিছুই টের পায় না। Subscription বদল তাই কোড বদলের মতোই review আর পরীক্ষার যোগ্য।

  4. বেছে নিন আর যুক্তি দিন: দুটি প্রবাহ (দুটিই কাল্পনিক)। (ক) একটি মোবাইল অ্যাপে নতুন ব্যবহারকারী নিবন্ধন করলে: স্বাগত SMS, loyalty অ্যাকাউন্ট খোলা, analytics-এ গোনা, আর ভবিষ্যতে হয়তো আরও কিছু। (খ) একটি ছোট ঋণ বিতরণ: KYC যাচাই → credit check → ledger-এ টাকা বরাদ্দ → wallet-এ পাঠানো → গ্রাহককে জানানো; যেকোনো ধাপ ব্যর্থ হলে আগের ধাপ উল্টাতে হবে, আর প্রতিটি ধাপের সময়সীমা আছে। প্রতিটির জন্য choreography না orchestration, আর কেন? Event, command আর ব্যর্থতার পথ সংক্ষেপে লিখুন।

    (ক) Choreography — pub/sub fan-out. User সেবা UserRegistered (id, সময়, channel; ফোন নম্বর নয়) প্রকাশ করে; SMS, loyalty, analytics নিজের subscription-এ শোনে। ধাপগুলোর মধ্যে কোনো নির্ভরতা বা compensation নেই; একটি ব্যর্থ হলে বাকিরা চলে, আর প্রতিটি নিজে retry আর DLQ সামলায়। নতুন শ্রোতা এলে user সেবা অপরিবর্তিত — choreography-র সবচেয়ে বড় সুবিধাটাই এখানে কাজে লাগে। শ্রোতারা event id দিয়ে idempotent, যাতে দুবার স্বাগত SMS না যায়।

    (খ) Orchestration. পাঁচটি ধাপ, কঠোর ক্রম, টাকা, compensation আর সময়সীমা — আর “ঋণ L-77 কোথায়?” প্রশ্নের উত্তর গ্রাহক-সেবা আর নিরীক্ষকের লাগবে। একটি loan orchestrator (বা workflow engine) টেকসই সারি রাখে: VerifyKyc → CheckCredit → ReserveFunds → Disburse → Notify — সবগুলো command, idempotency key L-77-<ধাপ>-সহ। ব্যর্থতায় উল্টো ক্রমে: Disburse ব্যর্থ হলে ReleaseFunds (ledger-এ উল্টো entry, মোছা নয়)। প্রতিটি ধাপে orchestrator-এর timer; সময় পেরোলে retry, তারপর compensation বা মানুষের হাতে। শেষে সে LoanDisbursed বা LoanRejected event প্রকাশ করে, যা বাইরের শ্রোতারা শোনে — ভেতরে orchestration, সীমানায় event.

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমস্বাক্ষরযুক্ত একটি webhook গ্রহণের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. কাঁচা body আর স্বাক্ষর ও timestamp-এর header পড়া, JSON parse না করে
  2. Timestamp কয়েক মিনিটের বেশি পুরোনো হলে প্রত্যাখ্যান (সস্তা পরীক্ষা, তাই HMAC-এর আগেই)
  3. গোপন চাবি দিয়ে timestamp আর কাঁচা body-র HMAC হিসাব করা
  4. হিসাবটি header-এর স্বাক্ষরের সাথে constant-time-এ তুলনা
  5. Event id inbox-এ আছে কি না দেখা; থাকলে duplicate হিসেবে বাদ
  6. Event id লিখে কাজটি queue-তে রাখা আর দ্রুত 2xx উত্তর দেওয়া

একটি broker-এ একটি বার্তা আসে। ব্যবস্থা ক-তে একটি queue, যেখান থেকে ৩টি কর্মী একসাথে বার্তা তোলে। ব্যবস্থা খ-তে একটি topic, যার ৩টি আলাদা subscription-এর প্রতিটিতে একটি করে কর্মী। প্রতিটি ব্যবস্থায় বার্তাটি মোট কতবার প্রক্রিয়া হবে (duplicate বাদে)?

  • ক-তে ১ বার, খ-তে ১ বার
  • ক-তে ৩ বার, খ-তে ৩ বার
  • ক-তে ১ বার, খ-তে ৩ বার
  • ক-তে ৩ বার, খ-তে ১ বার

একটি topic-এ সেকেন্ডে ৫০০টি event আসে, প্রতিটি ৪ KB, আর ৬টি subscription সব event চায়। Broker থেকে subscriber-দের দিকে মোট bandwidth কত (১ MB = ১,০০০ KB)?

  • সেকেন্ডে ২ MB
  • সেকেন্ডে ১২ MB
  • সেকেন্ডে ৩ MB
  • সেকেন্ডে ৪৮ MB

নিচের কোন বার্তার নামটি একটি event-এর জন্য সবচেয়ে উপযুক্ত?

  • ReserveSeat
  • SendSmsRequested
  • BookingTableRowUpdated
  • SeatReserved

একটি দল event-এ শুধু booking id পাঠানোর বদলে শ্রোতাদের দরকারি সব তথ্য (রুট, সিট, দাম) পাঠানো শুরু করল (event-carried state transfer). এর প্রধান লাভ আর একটি প্রধান দাম কী?

  • লাভ: শ্রোতারা প্রকাশককে না ডেকেই চলতে পারে; দাম: ডেটার পুরোনো হতে পারা কপি আর বড় payload-এর চুক্তি
  • লাভ: schema বদল সহজ হয়; দাম: event ধীরে যায়
  • লাভ: duplicate আর আসে না; দাম: broker-এর খরচ বাড়ে
  • লাভ: সব সেবায় strong consistency; দাম: কোনোটি নেই

একটি বুকিং প্রবাহ choreography-তে চলে: Order → (OrderPlaced) → Seat → (SeatHeld) → Payment → (PaymentCharged) → Ticket. Ticket সার্ভিস হঠাৎ কোনো event প্রকাশ না করে নিঃশব্দে থেমে গেল। আলাদা কোনো নজরদারি নেই। কী ঘটে?

  • Order সার্ভিস timeout ধরে নিজে থেকে compensation শুরু করে
  • প্রবাহ নিঃশব্দে আটকে থাকে — সিট hold, টাকা কাটা, অর্ডার PENDING; কেউ টের পায় না
  • Broker নিজে TicketFailed event তৈরি করে পাঠায়
  • Payment সার্ভিস ব্যাপারটি টের পেয়ে টাকা ফেরত দেয়

একটি webhook প্রদাতা স্বাক্ষর হিসাব করে HMAC(secret, timestamp + "." + body) দিয়ে, শুধু body দিয়ে নয়। Timestamp-কে স্বাক্ষরের অংশ করার মূল কারণ কী?

  • যাতে body-র বিষয়বস্তু পথে কেউ পড়তে না পারে
  • যাতে event-গুলো সবসময় সঠিক ক্রমে পৌঁছায়
  • যাতে গ্রাহক পুরোনো, আগে পাঠানো বৈধ বার্তা সময় দেখে প্রত্যাখ্যান করতে পারে
  • যাতে গ্রাহককে event id দিয়ে duplicate খুঁজতে না হয়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Queue বনাম topic: queue-তে একটি বার্তা একজন কর্মী; topic-এ প্রতিটি subscription নিজের কপি, নিজের গতি। Push-এ দেরি কম, pull-এ batch আর backpressure; filter চলে topic hierarchy বা attribute-এ।
  2. Event ≠ command ≠ query: event অতীতের সত্য (SeatHeld), command অনুরোধ, query প্রশ্ন। Event notification, event-carried state transfer আর event sourcing — তিনটি আলাদা সিদ্ধান্ত।
  3. ভালো event: অতীত কালের ব্যবসায়িক নাম, অনন্য id, সময়, source, type, version, correlation আর causation id; ভেতরের ডেটাবেসের সারি নয়, ব্যক্তিগত তথ্য যতটা সম্ভব কম।
  4. Fan-out = E × N: delivery, bandwidth আর জমা কপি subscription-এর সংখ্যায় গুণ হয়; প্রতিটি subscription-এর retry, DLQ আর backlog monitoring চাই।
  5. Choreography বনাম orchestration: কেন্দ্রহীন শিকল সহজে বাড়ে কিন্তু পুরো অবস্থা কেউ জানে না; orchestrator সব জানে আর timer রাখে। Temporal decoupling-এর দাম eventual consistency, duplicate, ক্রম আর tracing; webhook-এ স্বাক্ষর, timestamp আর retry.

পরের পর্বে: পর্ব ৩৩ — Log-ভিত্তিক streaming: partition, offset, consumer group আর retention. Event মুছে না ফেলে একটি লম্বা log-এ রেখে দিলে কী হয় — একই কপি থেকে অনেক গ্রাহক, অতীত থেকে আবার পড়া, আর partition দিয়ে ক্রম ও scale — দেখব।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান