পাঠ ২৭ · ৫৮-এর মধ্যে · মডিউল ৬
Home / Courses / Software Engineering Principles & Git / সফটওয়্যার আর্কিটেকচার

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

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

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

  • ইভেন্ট-ড্রিভেন আর্কিটেকচার কী এবং এটি ডাইরেক্ট মেথড-কল থেকে কীভাবে ভিন্ন
  • producer, consumer ও broker/bus — এই তিনটি উপাদান কীভাবে একসাথে কাজ করে
  • এটি কীভাবে Observer প্যাটার্নের একটি আর্কিটেকচার-স্কেল, আরও লুজলি-কাপলড সংস্করণ
  • একটি বাস্তব Python EventBus বানিয়ে দেখানো, নতুন consumer যোগ করলেও producer অপরিবর্তিত থাকে

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

ইভেন্ট-ড্রিভেন আর্কিটেকচারEvent-Driven Architectureএকটি আর্কিটেকচারাল স্টাইল যেখানে কম্পোনেন্টগুলো একে অপরের মেথড সরাসরি কল না করে "ইভেন্ট" তৈরি ও গ্রহণ করে যোগাযোগ করে। একটি আর্কিটেকচারাল স্টাইল, যেখানে সিস্টেমের কম্পোনেন্টগুলো একে অপরের মেথড সরাসরি কল করার বদলে ইভেন্ট — অর্থাৎ "কিছু একটা ঘটেছে" এই তথ্যের একটি রেকর্ড, যেমন "OrderPlaced" বা "PaymentReceived" — তৈরি ও গ্রহণ করে যোগাযোগ করে। M6/L25-এর লেয়ার্ড আর্কিটেকচারে যেমন প্রতিটি লেয়ার নিচের লেয়ারকে সরাসরি চেনে ও কল করে, এখানে ঠিক তার উল্টো — একটি কম্পোনেন্ট শুধু ঘোষণা করে "এই ঘটনাটি ঘটেছে," আর কে সেটি নিয়ে কী করবে তা সেই কম্পোনেন্টের জানারই দরকার নেই।

২ · মূল উপাদান — Producer, Consumer, Broker/Bus

তিনটি অংশ নিয়ে এই আর্কিটেকচার গঠিত (প্রতিটি নির্দিষ্ট করে বলা প্রয়োজন):

Event ProducerEvent Producerএকটি কম্পোনেন্ট যা কিছু ঘটেছে তা শনাক্ত করে একটি ইভেন্ট emit/publish করে, কে সেটি consume করবে তা না জেনেই। (Publisher)
কিছু ঘটেছে শনাক্ত করে একটি ইভেন্ট emit করে — কে (যদি কেউ থাকে) সেটি consume করবে, তা জানারও দরকার নেই।
Event ConsumerEvent Consumerএকটি কম্পোনেন্ট যা নির্দিষ্ট ইভেন্ট টাইপে আগ্রহ নিবন্ধন (subscribe) করে এবং সেই ইভেন্ট ঘটলে সাড়া দেয়। (Subscriber)
নির্দিষ্ট ইভেন্ট টাইপে subscribe করে রাখে, এবং সেই ইভেন্ট ঘটলে সাড়া দেয়।
Event Broker/BusEvent Broker / Message Busproducer আর consumer-এর মাঝে থাকা একটি ইন্টারমিডিয়ারি যা ইভেন্ট রাউট করে — producer আর consumer একে অপরের সরাসরি রেফারেন্স রাখে না।
মাঝখানে বসে ইভেন্ট রাউট করে — producer আর consumer একে অপরকে সরাসরি চেনেই না, শুধু broker-কে চেনে। বাস্তব সিস্টেমে এটি প্রায়ই একটি "message queue" প্রযুক্তি হিসেবে বাস্তবায়িত হয় — এর আসল অবকাঠামো ও টুলিং Cloud Computing & DevOps কোর্সে।
OrderService (Producer) EventBus — "OrderPlaced" রাউট করে EmailService InventoryService AnalyticsService নতুন যোগ হলো, ০ পরিবর্তন
OrderService শুধু EventBus-কে চেনে — EmailService আর InventoryService কে, তা জানেই না। AnalyticsService নতুন যোগ হলেও OrderService-এর কোডে কোনো পরিবর্তন লাগে না।

৩ · এটি আসলে Observer প্যাটার্নের আর্কিটেকচার-স্কেল সংস্করণ

M5/L23-এর সাথে সরাসরি সম্পর্ক

Observer প্যাটার্নObserver Patternএকটি সাবজেক্টের state বদলালে তার রেজিস্টার্ড observer-দের স্বয়ংক্রিয়ভাবে জানানো — M5/L23-এ দেখা একটি বিহেভিয়ারাল ডিজাইন প্যাটার্ন। (M5/L23) মনে আছে? ইভেন্ট-ড্রিভেন আর্কিটেকচার আসলে সেই একই ধারণা — কিন্তু এখন একটি ক্লাসের বদলে পুরো একটি সিস্টেম/সার্ভিসের স্কেলে। তবে একটি গুরুত্বপূর্ণ পার্থক্য আছে: Observer প্যাটার্নে subject নিজে সরাসরি observer-দের একটি লিস্ট রেফারেন্স হিসেবে ধরে রাখে (subscribe() কল করলে subject-এর নিজের লিস্টে যোগ হয়)। ইভেন্ট-ড্রিভেন আর্কিটেকচারে producer consumer-দের কোনো লিস্টই রাখে না — সে শুধু broker-কে ইভেন্ট পাঠায়, broker রাউটিং সামলায়। ফলে কাপলিং আরও কম — producer আক্ষরিক অর্থে জানেই না যে কোনো consumer আদৌ আছে কিনা।

৪ · সুবিধা — স্বাধীন স্কেলিং ও শূন্য-পরিবর্তনে সম্প্রসারণ

M6/L26-এ মাইক্রোসার্ভিসের একটি সুবিধা ছিল স্বাধীন স্কেলিং — ইভেন্ট-ড্রিভেন আর্কিটেকচার এই সুবিধাকে আরও এগিয়ে নেয়: producer আর consumer একে অপরকে সরাসরি রেফারেন্স করে না বলে তাদের আলাদাভাবে ডেভেলপ, ডিপ্লয় ও স্কেল করা যায়। আর সবচেয়ে গুরুত্বপূর্ণ প্র্যাকটিক্যাল সুবিধা — একটি বিদ্যমান ইভেন্ট টাইপের জন্য একটি নতুন consumer যোগ করতে producer-এর কোডে শূন্য পরিবর্তন লাগে — এটি L16-এর Open/Closed PrincipleOCPসফটওয়্যার এনটিটি এক্সটেনশনের জন্য open, মডিফিকেশনের জন্য closed থাকা উচিত। -এর সরাসরি বাস্তবায়ন, কিন্তু এখন ক্লাস-লেভেলে নয়, পুরো আর্কিটেকচার-লেভেলে।

নিচের কোড সেলে একটি বাস্তব EventBus বানানো হয়েছে — এটি স্যান্ডবক্স-সিমুলেশন নয়, এটি ordinary, সম্পূর্ণ কার্যকরী Python OOP, ঠিক Observer-এর মতোই।

Python
class EventBus:
    def __init__(self):
        self.subscribers = {}  # event_type -> handler function-দের লিস্ট

    def subscribe(self, event_type, handler):
        self.subscribers.setdefault(event_type, []).append(handler)

    def publish(self, event_type, data):
        for handler in self.subscribers.get(event_type, []):
            handler(data)


class OrderService:
    def __init__(self, bus):
        self.bus = bus  # OrderService শুধু bus-কে চেনে, কোনো consumer-কে সরাসরি চেনে না

    def place_order(self, order_id, item):
        print(f"OrderService: অর্ডার #{order_id} তৈরি হলো ({item})")
        self.bus.publish("OrderPlaced", {"order_id": order_id, "item": item})


class EmailService:
    def handle_order_placed(self, data):
        print(f"  EmailService: কনফার্মেশন ইমেইল পাঠানো হলো -- অর্ডার #{data['order_id']}")


class InventoryService:
    def handle_order_placed(self, data):
        print(f"  InventoryService: স্টক থেকে '{data['item']}' বিয়োগ করা হলো")


class AnalyticsService:
    def handle_order_placed(self, data):
        print(f"  AnalyticsService: অর্ডার #{data['order_id']} অ্যানালিটিক্সে লগ করা হলো")


bus = EventBus()
order_service = OrderService(bus)
email_service = EmailService()
inventory_service = InventoryService()

# ধাপ ১: দুইজন consumer সাবস্ক্রাইব করছে
bus.subscribe("OrderPlaced", email_service.handle_order_placed)
bus.subscribe("OrderPlaced", inventory_service.handle_order_placed)

print("== প্রথম অর্ডার (২ জন consumer সাবস্ক্রাইবড) ==")
order_service.place_order(101, "ল্যাপটপ")

# ধাপ ২: তৃতীয় consumer যোগ -- OrderService-এর কোডে কোনো পরিবর্তন লাগেনি
analytics_service = AnalyticsService()
bus.subscribe("OrderPlaced", analytics_service.handle_order_placed)

print()
print("== দ্বিতীয় অর্ডার (৩ জন consumer সাবস্ক্রাইবড, OrderService অপরিবর্তিত) ==")
order_service.place_order(102, "মাউস")

    
লক্ষ্য করুন OrderService.place_order() মেথডে EmailService, InventoryService, বা AnalyticsService — কারও নামই কোথাও লেখা নেই। এটিই এই আর্কিটেকচারের মূল কথা — producer শুধু self.bus.publish(...) কল করে, তারপর সম্পূর্ণ ভুলে যায় কে সেই ইভেন্ট নিয়ে কী করছে।
মূল কথা · Key takeaway

ইভেন্ট-ড্রিভেন আর্কিটেকচার producer আর consumer-কে একটি broker/bus দিয়ে আলাদা করে দেয় — ফলে তারা একে অপরকে সরাসরি না চিনেই যোগাযোগ করতে পারে। এটি M5/L23-এর Observer প্যাটার্নের একটি আরও লুজলি-কাপলড, আর্কিটেকচার-স্কেল সংস্করণ, এবং L16-এর OCP-কে বাস্তবে প্রমাণ করে — একটি বিদ্যমান ইভেন্টের জন্য নতুন consumer যোগ করা মানেই producer-এর একটি লাইনও বদলানো নয়।

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

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

প্র ০১ Observer প্যাটার্ন (M5/L23) আর ইভেন্ট-ড্রিভেন আর্কিটেকচারের মধ্যে ঠিক কোন কাপলিং-এর পার্থক্যটি সবচেয়ে গুরুত্বপূর্ণ?

Observer-এ subject নিজে সরাসরি তার observer-দের একটি লিস্ট রেফারেন্স হিসেবে ধরে রাখে — subject জানে ঠিক কারা তাকে observe করছে। ইভেন্ট-ড্রিভেন আর্কিটেকচারে producer কোনো consumer-এর লিস্টই রাখে না — সে শুধু broker-কে ইভেন্ট পাঠায়, broker রাউটিং সামলায়। ফলে producer আক্ষরিক অর্থে জানেই না কোনো consumer আদৌ আছে কিনা, এমনকি কতজন আছে তাও না — এটি Observer-এর চেয়েও একধাপ কম কাপলিং।

প্র ০২ উপরের কোড সেলে যদি AnalyticsService যোগ করার বদলে EmailService-কে সরিয়ে দেওয়া হতো, তাহলে OrderService-এর কোডে কি কোনো পরিবর্তন লাগত?

না। শুধু bus.unsubscribe(...)-জাতীয় একটি কল (বা সাবস্ক্রাইব লিস্ট থেকে সরানো) দরকার হতো — OrderService.place_order() মেথডে কোনো পরিবর্তনই লাগত না, কারণ সে কখনোই EmailService-কে সরাসরি চেনেনি। এটিই decoupling-এর আসল প্র্যাকটিক্যাল সুবিধা — consumer যোগ করা বা সরানো, দুটোই producer-এর জন্য সম্পূর্ণ অদৃশ্য।

প্র ০৩ ইভেন্ট-ড্রিভেন আর্কিটেকচারের কোনো সততার সাথে বলা weakness/trade-off কী হতে পারে, শুধু সুবিধা নয়?

যেহেতু producer জানেই না কারা consume করছে, তাই একটি ইভেন্ট প্রসেস হওয়ার পুরো ফ্লো ট্রেস করা কঠিন হতে পারে — একটি বাগ ডিবাগ করতে গেলে বুঝতে হয় ঠিক কতজন consumer সেই ইভেন্টে সাবস্ক্রাইবড এবং প্রতিটি কী করছে, যা M6/L25-এর লেয়ার্ড আর্কিটেকচারের সরাসরি, অনুমানযোগ্য কল-চেইনের তুলনায় কম স্বচ্ছ। এটি genuinely একটি অপারেশনাল জটিলতা, শুধু একটি নিখুঁত সমাধান হিসেবে দেখা উচিত নয়।

অনুশীলন

  1. চিন্তা করুন: একটি ই-কমার্স সিস্টেমে "PaymentReceived" নামে একটি নতুন ইভেন্ট টাইপ চালু করা হলে, কোন কোন consumer সেই ইভেন্টে আগ্রহী হতে পারে তার একটি তালিকা করুন (উদাহরণ: একটি রসিদ পাঠানো, একটি শিপিং প্রসেস শুরু করা)।

    সম্ভাব্য consumer: একটি ReceiptService (গ্রাহককে রসিদ ইমেইল করে), একটি ShippingService (শিপমেন্ট প্রসেস শুরু করে), একটি FraudDetectionService (সন্দেহজনক পেমেন্ট প্যাটার্ন পরীক্ষা করে), এবং একটি AccountingService (রাজস্ব রেকর্ড আপডেট করে) — এই প্রতিটিই স্বাধীনভাবে "PaymentReceived" ইভেন্টে সাবস্ক্রাইব করতে পারে, কোনোটিই পেমেন্ট প্রসেস করা কম্পোনেন্টের কোডকে সরাসরি স্পর্শ না করেই।

  2. পরীক্ষা করুন: উপরের কোড সেলে একটি নতুন ShippingService ক্লাস (নিজের একটি handle_order_placed মেথডসহ) যোগ করে সেটিকে বাসে সাবস্ক্রাইব করে দেখুন — order_service.place_order(...) কল একই রেখে আউটপুটে চতুর্থ consumer-ও সাড়া দিচ্ছে কিনা যাচাই করুন।

    নতুন ShippingService ক্লাস যোগ করে bus.subscribe("OrderPlaced", shipping_service.handle_order_placed) কল করলেই যথেষ্ট — পরবর্তী order_service.place_order(...) কলের আউটপুটে এখন চারজন consumer-ই সাড়া দেবে, এবং OrderService ক্লাসের একটি লাইনও বদলাতে হবে না — কোড সেলের মূল দাবিটির সরাসরি প্রমাণ।

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

আগের পাঠ
মাইক্রোসার্ভিস বনাম মনোলিথ