python 112 lines · 3 tabs

Synchronous Domain Event Bus with a Registry-Based Publisher in Python

Shared by codesnips Jul 2026
3 tabs
from dataclasses import dataclass, field
from datetime import datetime, timezone
from decimal import Decimal


def _now():
    return datetime.now(timezone.utc)


@dataclass(frozen=True)
class DomainEvent:
    occurred_at: datetime = field(default_factory=_now)


@dataclass(frozen=True)
class OrderPlaced(DomainEvent):
    order_id: str = ""
    customer_email: str = ""
    total: Decimal = Decimal("0")
    line_items: tuple = ()


@dataclass(frozen=True)
class PaymentCaptured(DomainEvent):
    order_id: str = ""
    amount: Decimal = Decimal("0")
    processor_ref: str = ""
3 files · python Explain with highlit

This snippet builds a small in-process domain event bus: events are plain immutable value objects, subscribers register handlers by event type, and a central EventBus fans out each published event to every interested handler. It is the kind of thing a service reaches for when different parts of an application need to react to "something happened" without the code that caused it knowing about the code that responds.

In events.py, each domain event is a frozen @dataclass, so instances are immutable and cheap to construct. Freezing matters: a published event is a fact about the past, and no subscriber should be able to mutate it and change what another subscriber sees. DomainEvent is a common base carrying an occurred_at timestamp, while concrete events like OrderPlaced and PaymentCaptured add their own fields. Using types as the routing key (rather than magic strings) keeps dispatch refactor-safe and lets editors autocomplete.

event_bus.py is the core. EventBus keeps a defaultdict mapping an event class to a list of handlers. subscribe registers a callable for a type, and the handles decorator is sugar over that so a function can declare its interest inline. publish looks up handlers by type(event) and calls each one; _dispatch wraps every handler in a try/except so one failing subscriber cannot stop the others, collecting failures instead. This isolation is the key trade-off of a synchronous bus — handlers run in the caller's thread, in order, but errors must be contained deliberately.

app.py shows the wiring. Handlers such as send_confirmation_email and update_inventory subscribe via @bus.handles(OrderPlaced), and the order service simply calls bus.publish(OrderPlaced(...)) after doing its work. The caller stays ignorant of who listens, which is exactly the decoupling the observer pattern buys.

The pitfalls are worth naming. This bus is synchronous and single-process, so slow handlers block the publisher and nothing survives a crash — for durability or cross-service delivery a real message queue or transactional outbox is needed. It also dispatches on exact type, not subclasses, so an OrderPlaced handler will not receive a hypothetical subclass. Within those limits it is a clean, dependency-free way to keep domain logic focused while side effects live in isolated, testable handlers.


Related snips

Share this code

Here's the card — post it anywhere.

Synchronous Domain Event Bus with a Registry-Based Publisher in Python — share card
Link copied