Domain Events & Event Design
An event is a record that something meaningful happened: OrderPlaced, PaymentFailed, SubscriptionRenewed. In domain-driven design, domain events capture facts the business cares about, in the business's own language. In event-driven architectures, events are how services learn about each other's changes without tight coupling.
The quality of an event-driven system depends heavily on event design: what events exist, what they're called, what data they carry, and what metadata surrounds them. Poorly designed events (vague EntityUpdated messages, commands disguised as events, payloads that leak internal models) create coupling, ambiguity, and painful evolution. Well-designed events make systems understandable, and let new consumers appear without changing producers.
TL;DR
- Events are immutable facts in the past tense, named in ubiquitous language:
InvoiceSent, notSendInvoiceorInvoiceUpdated. - Events ≠ commands: a command asks for something to happen (and can be rejected), while an event states that it happened.
- Domain events are internal to a bounded context. Integration events are published contracts for other services.
- Fat events carry state (consumers need no callbacks). Thin events carry IDs (consumers fetch details). Choose deliberately.
- Include metadata: event ID, type, version, occurred-at, aggregate ID and version, correlation and causation IDs.
- Design for ordering per aggregate, idempotent consumers, and schema evolution.
Quick Example
An integration event with an envelope and a fat payload:
Raising domain events from an aggregate (TypeScript):
Core Concepts
Events vs Commands vs Queries
Disguised commands like SendWelcomeEmailEvent couple the producer to a specific consumer's behavior. Publish CustomerRegistered, and let the email service decide to send a welcome email.
Domain Events vs Integration Events
- Domain events live within a bounded context, and trigger side effects inside it (update another aggregate, recalculate totals). They can be fine-grained, and change freely with the model.
- Integration events cross service boundaries. They're public contracts: stable, versioned, documented, and deliberately shaped for consumers. Often a domain event is translated into an integration event at the boundary, possibly aggregating or filtering information.
Naming and Granularity
- Use business language in the past tense:
ShipmentDispatched,ClaimApproved,SeatReserved. - Prefer specific, intent-revealing events over generic CRUD ones:
CustomerAddressCorrectedversusCustomerRelocatedmight trigger different downstream behavior (tax recalculation), whileCustomerUpdatedhides which happened. - Avoid events that are too fine-grained (one per field change) or too coarse (one mega-event per aggregate change).
Payload Styles
Many teams favor reasonably fat integration events containing what consumers commonly need, which lets services work when the producer is down. See microservices data management.
Metadata
Standard metadata makes events traceable and processable:
- Event ID (unique, for idempotency) and type.
- Schema version (see event schema evolution).
- Occurred-at timestamp (business time) and, separately, publish time.
- Aggregate ID and version, for ordering and gap detection.
- Correlation ID (ties all events and commands in one business flow) and causation ID (the message that directly caused this one), which are invaluable for debugging sagas and distributed traces.
CloudEvents standardizes much of this envelope.
Ordering and Delivery
Brokers generally guarantee ordering only within a partition, so key events by aggregate ID. Consumers must handle duplicates (at-least-once delivery) and occasionally out-of-order events across aggregates. Aggregate versions let consumers ignore stale updates. Reliable publishing uses the outbox pattern.
Publishing From Aggregates
In DDD, aggregates record domain events as part of state changes, and the application layer dispatches them after (or atomically with) persistence. Dispatch within the transaction only for handlers in the same consistency boundary. Cross-boundary handlers should react asynchronously, via the outbox and broker.
Best Practices
Discover Events With Domain Experts
Event storming workshops map a business process as a timeline of domain events with commands, actors, and policies. It's a fast way to find the right events and bounded contexts.
Publish Facts, Not Instructions
Producers describe what happened, and consumers own their reactions. This keeps producers stable as new consumers appear.
Keep Personal Data Minimal
Events are copied widely and retained long. Include only the necessary personal data, or use references and encryption to support deletion obligations.
Document Your Event Catalog
Maintain a catalog of events (AsyncAPI specs, EventCatalog, or registry docs) with owners, schemas, examples, and consumers. Discoverability prevents duplicate or conflicting events.
Common Mistakes
CRUD Events for Everything
OrderCreated, OrderUpdated, and OrderDeleted force consumers to diff payloads to infer what happened. Model meaningful transitions instead.
Exposing Internal Models
Serializing the internal ORM entity as the event payload couples every consumer to your database schema. Design explicit event contracts.
Relying on Global Ordering
Assuming all events across all aggregates arrive in order breaks under partitioning and retries. Design consumers for per-key ordering and idempotency.
FAQ
What is a domain event?
A record of something significant that happened in the business domain, named in the past tense using the domain's language (for example OrderShipped). Domain events capture business facts, and let other parts of the system react without the originating code knowing about them.
What's the difference between an event and a command?
A command is a request for an action directed at a specific handler, and it can be rejected. An event announces that something has already happened, and it's broadcast to any interested subscribers, so it can't be rejected, only reacted to.
Should events contain full data or just IDs?
It depends on consumer needs and coupling trade-offs. Thin events with IDs keep payloads small, but force consumers to call back to the producer. Fat events carry the relevant state, so consumers work independently. Many systems use moderately fat integration events for autonomy.
How should I name events?
Use past-tense verbs with business meaning, scoped to the domain: PaymentAuthorized, SubscriptionCancelled, ParcelDelivered. Avoid technical or generic names like DataChanged or RecordUpdated, and never name events as instructions.
Related Topics
- Event-Driven Architecture — Pillar overview
- Domain-Driven Design — Aggregates and bounded contexts
- Event Sourcing — Events as the source of truth
- Event Schema Evolution — Changing event contracts safely
- Outbox Pattern — Reliable publishing
- Choreography vs Orchestration — Coordinating with events