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.
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.
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.
- Order.pay() raises an OrderPaid event. The event waits.
- The unit of work commits the payment, and only then publishes the event.
- The event bus holds three handlers for OrderPaid.
- 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:
"""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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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
- Event Aggregator (Martin Fowler) (martinfowler.com)
- Domain Event (Martin Fowler) (martinfowler.com)
- Event Sourcing (Martin Fowler) (martinfowler.com)
- Publisher-Subscriber pattern (Microsoft Learn (Azure Architecture Center))
- Pattern: Transactional outbox (microservices.io (Chris Richardson))
- Node.js v24 documentation: Events (OpenJS Foundation)
- Python 3.14 documentation: exception groups (Python Software Foundation)
- Python 3.14 documentation: asyncio task groups and gather (Python Software Foundation)
- Python 3.14 documentation: sqlite3, how to use the connection context manager (Python Software Foundation)
- Guava API: EventBus (the section "Avoid EventBus") (Guava (Google))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress