Domain Events
Domain Events
"Domain Events capture the memory of something interesting that happened in the domain." — Fowler, DomainEvent (martinfowler.com/eaaDev/DomainEvent.html)
Intent
A Domain Event is an immutable record of a meaningful occurrence in the domain. Events are named in past tense — OrderConfirmed, CustomerRegistered, PaymentFailed — because they describe facts that have already happened, not commands or intentions. Each event carries an occurredOn timestamp and is immutable from the moment of creation.
Domain Events are emitted by Aggregate Roots to signal state changes. They decouple the Aggregate from downstream reactions: the Order aggregate knows that confirmation happened; it does not know who listens or what they do. Listeners might send a confirmation email, reduce inventory, or trigger a saga — the Aggregate is unaware of any of this.
Domain Events are the pivot point of the event-driven lineage chain in this vault. They extend the GoF Observer notification model backward (Observer-Pattern) and serve as the foundational primitive for Event Sourcing, CQRS projections, and Choreography Sagas forward.
When NOT to Use
- When you need synchronous in-process callbacks — use Observer or direct method calls; Domain Events add dispatch indirection that is unnecessary for simple in-process reactions.
- When the reaction is within the same Aggregate — if the aggregate needs to respond to its own state change, call internal methods directly. Domain Events exist to notify other parts of the system, not to wire an Aggregate to itself.
- Over-eventing (event proliferation) — publishing an event for every property mutation creates noise and tight consumer coupling. Events should represent meaningful business facts, not low-level field updates.
- When eventual consistency is unacceptable — Domain Events via deferred dispatch introduce at least one async boundary. If strict synchronous consistency across multiple aggregates is required, Domain Events are the wrong tool (though this often signals a modelling error).
When to Use
- Notifying other bounded contexts or services of state changes that occurred within this context.
- Triggering eventual-consistency workflows — one aggregate confirms an order, another aggregate decrements inventory, reacting to the
OrderConfirmedevent. - Building an audit trail — a Domain Event log is a natural, high-fidelity record of what happened in the domain.
- Decoupling the Aggregate from downstream reactions — the Aggregate does not import or reference event handlers.
- Enabling Event Sourcing (Phase 12) — when Domain Events are persisted as the source of truth, the Aggregate's state can be rebuilt by replaying its event stream.
How It Works
Participants:
- DomainEvent (interface/abstract class) — immutable; named in past tense; carries
occurredOn: Dateand any payload fields needed by consumers. The event object is the message — not just a "something changed" signal (contrast with GoF Observer's generic notification). - Aggregate Root — collects events in a private list during a business operation. Does NOT dispatch events immediately. After
order.confirm()runs, the Order holds anOrderConfirmedin its internal list; nothing has been published yet. - DomainEventPublisher (dispatcher) — dispatches events after the transaction commits (deferred dispatch). It reads the collected events from the Aggregate, publishes them to registered handlers, and clears the list.
Dispatch timing is critical:
- Deferred dispatch (recommended): Collect events on the Aggregate root during the transaction. After
save(order)commits successfully, extract events from the Aggregate and publish them. This ensures events are only dispatched for successful transactions. - Immediate dispatch (simpler, but risky): Dispatching inside the domain method — during the transaction — risks modifying two Aggregates in one transaction, violating Vernon Rule 4 (one Aggregate per transaction). Only use for read-only reactions.
Domain Event vs Integration Event:
| Concern | Domain Event | Integration Event |
|---|---|---|
| Scope | Intra-bounded-context | Inter-bounded-context (crosses service boundary) |
| Transport | In-memory dispatcher | Message bus (Kafka, RabbitMQ, SQS) |
| Consumer | Same application context | External services / microservices |
| Promotion | Domain event may be promoted → integration event at the context boundary |
A Domain Event stays inside the bounded context. If another service must react, the domain event is promoted to an Integration Event at the context boundary and published to a message bus. Domain Events and Integration Events are different objects — the Integration Event often carries a flattened, stable schema.
Sequence Diagram
sequenceDiagram
participant Client
participant Order as Order (Aggregate Root)
participant Repo as OrderRepository
participant Publisher as DomainEventPublisher
participant HandlerA as InventoryHandler
participant HandlerB as NotificationHandler
Client->>Order: confirm()
Note right of Order: Collects OrderConfirmed event<br/>in internal list (not dispatched yet)
Client->>Repo: save(order)
Repo->>Repo: persist to DB
Repo-->>Client: commit success
Client->>Order: clearEvents()
Order-->>Client: [OrderConfirmed]
Client->>Publisher: publish(OrderConfirmed)
Publisher->>HandlerA: handle(OrderConfirmed)
Note right of HandlerA: Reserve inventory
Publisher->>HandlerB: handle(OrderConfirmed)
Note right of HandlerB: Send confirmation email
Note over Order,Publisher: Deferred dispatch: events published<br/>only after transaction commits
TypeScript Example
// Domain Events — TypeScript (deferred dispatch pattern)
// Source: Vernon, IDDD, 2013; Stemmler, khalilstemmler.com
interface DomainEvent {
readonly occurredOn: Date;
}
class OrderConfirmed implements DomainEvent {
readonly occurredOn = new Date();
constructor(public readonly orderId: string) {}
}
// Simplified in-memory dispatcher
class DomainEventPublisher {
private handlers = new Map<string, Function[]>();
register(eventName: string, handler: Function): void {
const existing = this.handlers.get(eventName) ?? [];
this.handlers.set(eventName, [...existing, handler]);
}
publish(event: DomainEvent): void {
const name = event.constructor.name;
(this.handlers.get(name) ?? []).forEach(h => h(event));
}
}
// Events collected from Aggregate after transaction commit, then dispatchedJava Example
// Domain Events — Java (custom base class + Spring ApplicationEvent alternative)
// Source: Vernon, IDDD, 2013
abstract class DomainEvent {
private final Instant occurredOn = Instant.now();
public Instant getOccurredOn() { return occurredOn; }
}
class OrderConfirmed extends DomainEvent {
private final String orderId;
public OrderConfirmed(String orderId) { this.orderId = orderId; }
public String getOrderId() { return orderId; }
}
// Simplified in-memory publisher (application service dispatches after commit)
class DomainEventPublisher {
private final Map<Class<?>, List<Consumer<DomainEvent>>> handlers = new HashMap<>();
public <T extends DomainEvent> void subscribe(Class<T> type, Consumer<T> handler) {
handlers.computeIfAbsent(type, k -> new ArrayList<>()).add(e -> handler.accept(type.cast(e)));
}
public void publish(DomainEvent event) {
(handlers.getOrDefault(event.getClass(), List.of())).forEach(h -> h.accept(event));
}
}
// Spring alternative: extend ApplicationEvent; use ApplicationEventPublisher.publishEvent()
// @DomainEvents + @AfterDomainEventPublication on the Aggregate provides framework-managed deferred dispatchLineage Backward
Observer-Pattern — Domain Events extend the GoF Observer notification model. Observer notifies listeners of state changes via a generic interface; Domain Events give those notifications explicit names, immutable value semantics, and domain meaning. The event object itself is the message — not just a changed-state signal. Where Observer uses a generic update(observable, argument) callback, Domain Events use strongly-typed event classes named after business facts.
Lineage Forward
Event-Sourcing-Pattern (Phase 12) — Event Sourcing persists Domain Events as the source of truth; Aggregate state is rebuilt by replaying its event stream. Domain Events are the primitive that Event Sourcing stores and replays.
CQRS-Pattern (Phase 12) — CQRS projections are built by consuming Domain Events from the event store to produce read models. The write side emits Domain Events; the read side reacts to them to build eventually-consistent query views.
Choreography-Saga-Pattern (Phase 13) — Choreography Sagas coordinate distributed transactions by having services react to each other's Domain Events without a central orchestrator. Each step in the saga is triggered by a Domain Event emitted by the previous step's aggregate.
Related Concepts
| Pattern | Relationship |
|---|---|
| Observer-Pattern | GoF ancestor — generic notification mechanism; Domain Events add past-tense naming and immutable semantics |
| Integration Event | Cross-context promotion of a Domain Event — different object, stable schema, message-bus transport |
| Event-Sourcing-Pattern | Domain Events as the persistence mechanism — events stored rather than current state |
| Aggregate | Emitter of Domain Events — Aggregate Root collects events during state changes |
| CQRS-Pattern | Domain Events feed projections on the read side |
Related Architecture Patterns
- Modular-Monolith — Inter-module communication in a Modular Monolith uses Domain Events for eventual consistency; modules publish events and subscribing modules react without direct coupling across module boundaries.
- Vertical-Slice-Architecture — When one slice needs to trigger another, Domain Events are the correct mechanism; a slice publishes an event rather than importing another slice's handler, preventing cross-slice coupling.
Related System Design
- URL-Shortener-Design — URL expiry publishes a URLExpired domain event; background cleanup workers subscribe and delete the record — a direct application of domain event-driven coordination in a system design context
Sources
- Fowler, DomainEvent — https://martinfowler.com/eaaDev/DomainEvent.html
- Vernon, Implementing Domain-Driven Design, Addison-Wesley 2013 — Aggregate event collection and deferred dispatch
- microservices.io/patterns/data/domain-event.html — Domain Event pattern
Backlinks
- Observer-Pattern (GoF ancestor)
- Enterprise-Patterns-MOC