পাঠ ২৩ · ৫১-এর মধ্যে · মডিউল ৬
Home / Courses / System Design / ইভেন্ট-ড্রিভেন আর্কিটেকচার

ইভেন্ট-ড্রিভেন আর্কিটেকচার

Event-driven architecture
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • রিকোয়েস্ট-রেসপন্স (সিঙ্ক্রোনাস) মডেল বনাম ইভেন্ট-ড্রিভেন (অ্যাসিনক্রোনাস) মডেলের মৌলিক পার্থক্য
  • কেন ইভেন্ট-ড্রিভেন আর্কিটেকচার সার্ভিসগুলোর মধ্যে loose coupling তৈরি করে
  • "OrderPlaced" ইভেন্টের worked example দিয়ে বাস্তব ব্যবহার বোঝা
  • Python দিয়ে একটি ছোট ইভেন্ট বাস বাস্তবায়ন করা

১ · রিকোয়েস্ট-রেসপন্স বনাম ইভেন্ট-ড্রিভেন

এতদিন আমরা যে যোগাযোগ মডেল দেখেছি (HTTP/REST, gRPC — L06, L07) সেটি রিকোয়েস্ট-রেসপন্সRequest-Responseক্যালার একটি নির্দিষ্ট সার্ভিসকে সরাসরি কল করে এবং তার রেসপন্সের জন্য অপেক্ষা করে — সিঙ্ক্রোনাস, টাইট কাপলড। — ক্যালার জানে ঠিক কাকে কল করছে, এবং রেসপন্সের জন্য অপেক্ষা করে। এটি সিঙ্ক্রোনাস এবং টাইটলি কাপলড — যদি Service B ডাউন থাকে, Service A-এর কলও ব্যর্থ হয়।

ইভেন্ট-ড্রিভেন আর্কিটেকচারে Service A শুধু একটি ইভেন্ট প্রকাশ করে ("এই ঘটনাটি ঘটেছে") এবং জানেই না কে বা কতজন তা শুনছে। প্রতিটি আগ্রহী সার্ভিস স্বাধীনভাবে সেই ইভেন্টে প্রতিক্রিয়া জানায়, নিজের সময়ে, নিজের গতিতে।

Request-Response
ক্যালার সরাসরি একটি নির্দিষ্ট সার্ভিস কল করে ও রেসপন্সের অপেক্ষা করে — সিঙ্ক্রোনাস, টাইট কাপলড, ব্যর্থতা সরাসরি প্রোপাগেট হয়।
Event-Driven
প্রোডিউসার একটি ইভেন্ট প্রকাশ করে চলে যায় — কে শুনছে জানে না — প্রতিটি সাবস্ক্রাইবার স্বাধীনভাবে প্রতিক্রিয়া জানায়, লুজ কাপলড।
কেন এটি গুরুত্বপূর্ণ

রিকোয়েস্ট-রেসপন্স মডেলে যদি "OrderPlaced"-এর পর ৫টি ভিন্ন সার্ভিসকে কল করতে হয়, তাহলে অর্ডার সার্ভিসের কোড সেই ৫টি সার্ভিস সম্পর্কে জানতে হয়, এবং যেকোনো একটি ধীর/ডাউন হলে পুরো অর্ডার প্লেসমেন্ট আটকে যায়। ইভেন্ট-ড্রিভেন মডেলে অর্ডার সার্ভিস শুধু একটি ইভেন্ট প্রকাশ করে — নতুন একটি ৬ষ্ঠ সার্ভিস ভবিষ্যতে যোগ করতে চাইলে অর্ডার সার্ভিসের কোড এক লাইনও বদলাতে হয় না, শুধু নতুন সার্ভিসটি সেই ইভেন্টে সাবস্ক্রাইব করবে।

২ · Worked Example — "OrderPlaced" ইভেন্ট

একটি ই-কমার্স সিস্টেমে একজন গ্রাহক অর্ডার প্লেস করার সাথে সাথে "OrderPlaced" নামে একটি ইভেন্ট প্রকাশিত হয়। তিনটি সম্পূর্ণ স্বাধীন সার্ভিস এই ইভেন্টে প্রতিক্রিয়া জানায় — কেউ একে অপরকে সরাসরি কল করে না, সবাই শুধু একই ইভেন্ট শোনে।

অর্ডার সার্ভিস Order Service OrderPlaced Event Bus Inventory Decrement stock Notification Email customer Analytics Log the sale
অর্ডার সার্ভিস শুধু একটি ইভেন্ট প্রকাশ করে — তিনটি স্বাধীন সার্ভিস কেউ একে অপরকে না জেনেই প্রতিক্রিয়া জানায়।

৩ · Python-এ একটি ছোট ইভেন্ট বাস

নিচে একটি সাধারণ event bus বানানো হলো — একটি ডিকশনারি যা ইভেন্টের নামকে হ্যান্ডলার ফাংশনের তালিকার সাথে ম্যাপ করে। emit() কল করলে সেই ইভেন্টের সব রেজিস্টার্ড হ্যান্ডলার একে একে চলে।

Python
event_handlers = {}

def on(event_name, handler):
    event_handlers.setdefault(event_name, []).append(handler)

def emit(event_name, data):
    print(f"ইভেন্ট emit হলো: {event_name} | data={data}")
    for handler in event_handlers.get(event_name, []):
        handler(data)

def inventory_handler(data):
    print(f"  [Inventory] স্টক কমানো হচ্ছে: {data['item']} x{data['qty']}")

def notification_handler(data):
    print(f"  [Notification] ইমেইল পাঠানো হচ্ছে গ্রাহক {data['customer']}-কে")

def analytics_handler(data):
    print(f"  [Analytics] বিক্রয় লগ করা হলো: order #{data['order_id']}")

# তিনটি স্বাধীন হ্যান্ডলার একই ইভেন্টে রেজিস্টার — কেউ কাউকে চেনে না
on("OrderPlaced", inventory_handler)
on("OrderPlaced", notification_handler)
on("OrderPlaced", analytics_handler)

emit("OrderPlaced", {"order_id": 501, "item": "Book", "qty": 2, "customer": "Rahim"})

print(f"\nমোট {len(event_handlers['OrderPlaced'])}টি হ্যান্ডলার independently fire করলো — কেউ একে অপরকে ডাকেনি")

    
লক্ষ্য করুন — emit() ফাংশনটি নিজেও জানে না হ্যান্ডলারগুলো ঠিক কী করে; শুধু জানে সেগুলো কল করতে হবে। একটি চতুর্থ হ্যান্ডলার (যেমন loyalty-points আপডেট) ভবিষ্যতে যোগ করতে চাইলে শুধু on("OrderPlaced", new_handler) লিখলেই হবে — emit() বা অর্ডার সার্ভিসের একটি লাইনও বদলাতে হবে না। বাস্তব সিস্টেমে এই event_handlers dict-এর জায়গায় থাকে একটি Kafka টপিক বা RabbitMQ exchange (L22 দেখুন) যা নেটওয়ার্কের ওপারে থাকা আলাদা সার্ভিসগুলোকে ইভেন্ট ডেলিভার করে।

৪ · ট্রেড-অফ — সবকিছু ইভেন্ট-ড্রিভেন করা ঠিক নয়

ইভেন্ট-ড্রিভেন আর্কিটেকচারের একটি বাস্তব খরচ আছে — ডিবাগিং কঠিন হয় (একটি ইভেন্ট ঠিক কোন কোন হ্যান্ডলার ট্রিগার করলো তা ট্রেস করা সরাসরি ফাংশন কলের মতো সহজ নয়, তাই L41-এর ডিস্ট্রিবিউটেড ট্রেসিং এখানে বিশেষভাবে দরকারি হয়ে ওঠে), এবং eventual consistency মেনে নিতে হয় — ইভেন্ট emit হওয়ার সাথে সাথে সব হ্যান্ডলার সম্পন্ন হয়ে যাবে এই গ্যারান্টি নেই। যেখানে ইউজারকে সাথে সাথে একটি নির্দিষ্ট ফলাফল (যেমন "পেমেন্ট সফল হয়েছে কি না") জানাতে হয়, সেখানে সিঙ্ক্রোনাস রিকোয়েস্ট-রেসপন্সই সঠিক পছন্দ থেকে যায়।

মূল কথা · Key takeaway

ইভেন্ট-ড্রিভেন আর্কিটেকচার হলো "কে কী জানে" এই সম্পর্কটি উল্টে দেওয়া — প্রোডিউসার আর কনজিউমারদের চেনে না। এটি সার্ভিসগুলোকে স্বাধীনভাবে বিকশিত ও স্কেল হতে দেয় (মাইক্রোসার্ভিস আর্কিটেকচারের একটি মূল স্তম্ভ, L25-এ বিস্তারিত), কিন্তু বিনিময়ে ডিবাগযোগ্যতা ও তাৎক্ষণিক কনসিস্টেন্সি কিছুটা ছাড় দিতে হয়।

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

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

প্র ০১ Inventory হ্যান্ডলার যদি ব্যর্থ হয় (যেমন এক্সেপশন থ্রো করে), তাহলে Notification ও Analytics হ্যান্ডলারের কী হবে — এবং বাস্তব সিস্টেমে এটি কীভাবে সামলানো হয়?

উপরের সরল উদাহরণে যদি একটি হ্যান্ডলার এক্সেপশন থ্রো করে, লুপ থেমে যেতে পারে এবং পরের হ্যান্ডলারগুলো না-ও চলতে পারে — এটি একটি বাস্তব সমস্যা। প্রকৃত মেসেজ ব্রোকার (Kafka/RabbitMQ) ভিত্তিক সিস্টেমে প্রতিটি সাবস্ক্রাইবার আসলে আলাদা প্রসেস/সার্ভিস যে নিজের কপি স্বাধীনভাবে পড়ে — একটি সাবস্ক্রাইবার ক্র্যাশ করলে অন্যদের উপর কোনো প্রভাব পড়ে না, কারণ তারা একই in-process লুপে চলছে না। ব্যর্থ হ্যান্ডলারের বার্তা ব্রোকারে থেকে যায় এবং রিট্রাই হয় (L27-এর রিট্রাই প্যাটার্ন), বা বারবার ব্যর্থ হলে ডেড-লেটার-কিউতে যায়।

প্র ০২ ইভেন্ট-ড্রিভেন আর্কিটেকচার কীভাবে মাইক্রোসার্ভিস ডিজাইনের সাথে সম্পর্কিত?

মাইক্রোসার্ভিস আর্কিটেকচারে (L25-এ বিস্তারিত) প্রতিটি সার্ভিস স্বাধীনভাবে ডিপ্লয় ও স্কেল হয় — কিন্তু তাদের একে অপরের সাথে যোগাযোগ করতে হয়। যদি তারা শুধু সরাসরি সিঙ্ক্রোনাস কল ব্যবহার করে, তাহলে একটি সার্ভিস ডাউন হলে তার উপর নির্ভরশীল সবাই প্রভাবিত হয় — মাইক্রোসার্ভিসের "স্বাধীনতা"র সুবিধাই নষ্ট হয়ে যায়। ইভেন্ট-ড্রিভেন যোগাযোগ (Pub/Sub, L22) সেই স্বাধীনতা বজায় রাখে — প্রতিটি সার্ভিস অন্যদের অনলাইন থাকা না-থাকার উপর নির্ভর না করে নিজের সময়ে ইভেন্ট প্রসেস করতে পারে।

প্র ০৩ একটি নতুন ফিচার — "অর্ডার প্লেস হলে গ্রাহককে লয়্যালটি পয়েন্ট দেওয়া" — যোগ করতে হলে ইভেন্ট-ড্রিভেন সিস্টেমে কী পরিবর্তন লাগবে, আর একটি টাইট-কাপলড রিকোয়েস্ট-রেসপন্স সিস্টেমে কী লাগতো?

ইভেন্ট-ড্রিভেন সিস্টেমে শুধু একটি নতুন "Loyalty Service" বানিয়ে তাকে "OrderPlaced" ইভেন্টে সাবস্ক্রাইব করালেই হয় — অর্ডার সার্ভিসের কোড এক বিন্দুও বদলাতে হয় না। টাইট-কাপলড রিকোয়েস্ট-রেসপন্স সিস্টেমে অর্ডার সার্ভিসের কোডে সরাসরি একটি নতুন ফাংশন কল যোগ করতে হতো, অর্ডার সার্ভিসকে রিডিপ্লয় করতে হতো, এবং যদি নতুন Loyalty Service ধীর হয় তাহলে অর্ডার প্লেসমেন্টের লেটেন্সিও বেড়ে যেতো — এটাই loose coupling-এর বাস্তব সুবিধা।

অনুশীলন

  1. চিন্তা করুন: আপনার পরিচিত একটি অ্যাপে একটি ইভেন্ট (যেমন "UserSignedUp" বা "RideCompleted") চিহ্নিত করুন এবং সেই ইভেন্টে কমপক্ষে ৩টি স্বাধীন সার্ভিস কীভাবে প্রতিক্রিয়া জানাতে পারে তা লিখুন।

    উদাহরণ ("UserSignedUp" একটি রাইড-শেয়ারিং অ্যাপে): (১) Welcome Service — স্বাগত ইমেইল/SMS পাঠায়, (২) Promotions Service — প্রথম রাইডের জন্য একটি ডিসকাউন্ট কোড তৈরি করে, (৩) Analytics Service — সাইনআপ সংখ্যা ও উৎস (referral/organic) লগ করে ড্যাশবোর্ডে দেখায়। তিনটিই "UserSignedUp" ইভেন্টে স্বাধীনভাবে সাবস্ক্রাইব করে, একে অপরের অস্তিত্ব সম্পর্কে জানে না।

  2. কোড পরিবর্তন করুন: উপরের কোড সেলে একটি চতুর্থ হ্যান্ডলার loyalty_handler(data) যোগ করুন যা প্রিন্ট করে গ্রাহক কত পয়েন্ট পেলো (ধরুন প্রতি ইউনিট কোয়ান্টিটির জন্য ১০ পয়েন্ট), এবং সেটিকে "OrderPlaced" ইভেন্টে রেজিস্টার করুন। তারপর আবার emit() কল করে দেখুন সবগুলো (৪টি) হ্যান্ডলার ঠিকমতো ফায়ার করে কিনা।

    সমাধান: def loyalty_handler(data):
      points = data['qty'] * 10
      print(f" [Loyalty] {data['customer']} পেলো {points} পয়েন্ট")
    , তারপর on("OrderPlaced", loyalty_handler) যোগ করে আবার emit(...) কল করলে এবার চারটি হ্যান্ডলারই একে একে ফায়ার করবে — event_handlers["OrderPlaced"]-এর দৈর্ঘ্য হবে ৪, এবং অর্ডার সার্ভিসের মূল কোড (emit কলটি) একটুও বদলায়নি।

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

আগের পাঠ
মেসেজ কিউ ও Pub/Sub প্যাটার্ন