Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Design Patterns (GoF and modern)  Module 6 – Modern patterns for application code

In-process event bus and domain events

Publish and subscribe inside one program: a typed event bus, domain events raised by the model and sent after commit, and what to do when handlers fail.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free intro, Pro: 7 of 30 minutes free
Free: Publishers, subscribers and one bus between them. With a pass: the other 9 sections and 3 interview answers, about 23 minutes.

What you will learn

  • Implement a typed in-process event bus with subscribe, publish and unsubscribe
  • Publish domain events raised by the model only after a successful commit
  • Choose synchronous or asynchronous dispatch and isolate the failure of one handler
  • Explain what an in-process bus cannot promise compared with a message broker

Before you start

What you’ll learn · Before you start

What you will learn

  • Implement a typed in-process event bus with subscribe, publish and unsubscribe
  • Publish domain events raised by the model only after a successful commit
  • Choose synchronous or asynchronous dispatch and isolate the failure of one handler
  • Explain what an in-process bus cannot promise compared with a message broker

Before you start

On this page

An in-process event bus lets one part of a program announce that something happened while other parts react to it, and neither side knows the other: publishers and subscribers know only the bus and the event types. Domain events are the announcements a domain model makes, named in the past tense, such as OrderPaid. When an order is paid, an invoice, loyalty points and a text message must follow. With a bus, the order publishes one OrderPaid event and three handlers subscribe to it, so a fourth reaction is a new handler and no change to the order. Martin Fowler describes the same shape as an Event Aggregator, which channels the events of many objects through one object that clients subscribe to.

The examples ran on Python 3.14.8, and one on Node.js 24.21.0; all but the Node.js one also run in your browser.

Publishers, subscribers and one bus between them

Without a bus, Order.pay() calls the invoice service, the loyalty service and the text-message sender itself. Every new reaction edits the order’s code, every test of the order needs fakes for all three, and a failure in the message sender can stop a payment. With a bus, the order depends on one small object. Fowler’s Domain Event pattern describes an event as the memory of something interesting that affects the domain, so events are facts: immutable, and named for what already happened.

Order.pay() raises OrderPaid; the unit of work commits, then the event bus delivers it to invoice, loyalty and SMS handlers.Order.pay()raises OrderPaidUnit of workcommits the paymentEvent busOrderPaid: 3 handlersHandlers: they know the busand OrderPaid, not the orderInvoiceLoyaltypointsTextmessagethe event waitspublish after commitdelivers to each

A domain event from the order to three handlers, published after the commit

Text description of the diagram

The diagram reads from top to bottom.

  1. Order.pay() raises an OrderPaid event. The event waits.
  2. The unit of work commits the payment, and only then publishes the event.
  3. The event bus holds three handlers for OrderPaid.
  4. The bus delivers the event to each of the three handlers: Invoice, Loyalty points and Text message. The handlers know the bus and the OrderPaid event type, but not the order.

Adding a fourth reaction to a payment means subscribing a fourth handler; the order's code does not change.

The program builds a bus of ten lines, publishes two OrderPaid events and lets one handler unsubscribe between them:

One OrderPaid event, three handlers, and one handler that unsubscribes Python · opening/bus.py
"""An in-process event bus: an order publishes OrderPaid, and three handlers react without the order knowing any of
them. The handlers only print what real ones would do."""
from collections import defaultdict
from dataclasses import dataclass


@dataclass(frozen=True)
class OrderPaid:  # a domain event: named in the past tense, and never changed once raised
    order_id: str
    amount_paise: int
    customer: str


class EventBus:
    def __init__(self):
        self.handlers = defaultdict(list)  # event type -> handlers, in the order they subscribed

    def subscribe(self, event_type, handler):
        self.handlers[event_type].append(handler)
        return lambda: self.handlers[event_type].remove(handler)  # call it to unsubscribe

    def publish(self, event):
        for handler in tuple(self.handlers[type(event)]):
            handler(event)


class Order:
    def __init__(self, order_id, amount_paise, customer, bus):
        self.order_id, self.amount_paise, self.customer, self.bus = order_id, amount_paise, customer, bus
        self.status = "new"

    def pay(self):
        self.status = "paid"
        self.bus.publish(OrderPaid(self.order_id, self.amount_paise, self.customer))


def issue_invoice(event):
    print(f"  invoice: issued for {event.order_id}, {event.amount_paise / 100:.2f} rupees")


def award_points(event):
    print(f"  loyalty: {event.amount_paise // 10_000} points to {event.customer}")  # 1 point per 100 rupees


def send_sms(event):
    print(f"  sms: {event.customer}, your payment for {event.order_id} is received")


bus = EventBus()
bus.subscribe(OrderPaid, issue_invoice)
bus.subscribe(OrderPaid, award_points)
stop_sms = bus.subscribe(OrderPaid, send_sms)

print("A-1042 paid")
Order("A-1042", 125_000, "Asha", bus).pay()
stop_sms()  # Asha turned text messages off
print("A-1043 paid")
Order("A-1043", 49_900, "Asha", bus).pay()

Output

A-1042 paid
  invoice: issued for A-1042, 1250.00 rupees
  loyalty: 12 points to Asha
  sms: Asha, your payment for A-1042 is received
A-1043 paid
  invoice: issued for A-1043, 499.00 rupees
  loyalty: 4 points to Asha

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bus.py

The order called publish() and nothing else. The handlers ran in the order they subscribed, and after stop_sms() the second payment produced no text message. An in-process event bus is this small, which is both its appeal and its limit: it runs inside one process, during the publisher’s own call, and stores nothing.

A bus that real code can rely on needs five more decisions, and each has a name:

  • Dispatch after commit holds domain events until the transaction commits, so a payment that rolls back is never announced.
  • The snapshot rule stops a handler that unsubscribes during a publish from making the bus skip another handler.
  • Failure isolation lets every handler run when one of them fails, and tells the publisher about each failure.
  • Asynchronous dispatch spares the publisher the wait, but it changes what order and failure mean.
  • Durability is what an in-process bus lacks: waiting events die with the process, and redelivery needs idempotent handlers.

A failure drill: the handler that unsubscribed while the bus was publishing Pro: opens with a Pro, Premium or Ultimate pass

In this part:

  • Raising domain events in the model, dispatching them after commit
  • When a handler fails: stop, swallow or collect
  • Asynchronous handlers: what TaskGroup cancels, and the order messages arrive in
  • Node.js’s EventEmitter, checked against its documentation
  • What an in-process bus cannot promise, with a crash reproduced
  • Exercise: a typed bus with priorities and failure isolation
    • A typed event bus with priorities and failure isolation
  • Check yourself
  • Interview questions
    • What does an in-process event bus give you over calling the handlers directly, and what does it cost?
    • Customers received "payment received" text messages for payments that then rolled back. The SMS handler subscribes to OrderPaid on an in-process bus. What went wrong, and how do you fix it?
    • One of five OrderPaid handlers has started throwing exceptions, and sometimes the handlers after it do not run. Design the bus's failure policy.
    • When would you move from an in-process event bus to a message broker? What breaks if you swap one for the other and change nothing else?
  • Key takeaways

6 runnable examples (JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 21 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.