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 OrderConfirmed event.
  • 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: Date and 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 an OrderConfirmed in 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:

ConcernDomain EventIntegration Event
ScopeIntra-bounded-contextInter-bounded-context (crosses service boundary)
TransportIn-memory dispatcherMessage bus (Kafka, RabbitMQ, SQS)
ConsumerSame application contextExternal services / microservices
PromotionDomain 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 dispatched

Java 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 dispatch

Lineage 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.

PatternRelationship
Observer-PatternGoF ancestor — generic notification mechanism; Domain Events add past-tense naming and immutable semantics
Integration EventCross-context promotion of a Domain Event — different object, stable schema, message-bus transport
Event-Sourcing-PatternDomain Events as the persistence mechanism — events stored rather than current state
AggregateEmitter of Domain Events — Aggregate Root collects events during state changes
CQRS-PatternDomain Events feed projections on the read side
  • 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.
  • 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, DomainEventhttps://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