Skip to main content

Domain Events

Record important business occurrences that others should know about

TL;DR

Domain Events are immutable, time-stamped records of important business occurrences that something significant happened in the domain. Aggregates emit events when their state changes meaningfully (OrderConfirmed, PaymentProcessed, CustomerOnboarded). Other aggregates, services, and bounded contexts subscribe to these events and react asynchronously, enabling loose coupling and eventual consistency. Events decouple services, enable event sourcing for audit trails and temporal queries, and make workflows explicit. Use events instead of direct service calls to coordinate across aggregates and bounded contexts.

Learning Objectives

  • Model domain events using business language
  • Emit events from aggregates when significant changes occur
  • Implement asynchronous event handlers with eventual consistency
  • Distinguish domain events from technical infrastructure events
  • Use events for inter-aggregate and inter-context communication
  • Handle event versioning for long-term evolution
  • Implement event sourcing patterns

Motivating Scenario

You're building an order management system. When an order is confirmed, multiple things must happen: payment must be processed, inventory must be reserved, shipping must be arranged, the customer must be notified. If you implement this with direct service calls from Order to Payment, Inventory, Shipping, and Notification services, you create tight coupling. Every change to the order process requires modifying the Order aggregate or orchestration service. Instead, the Order aggregate emits an OrderConfirmed event. Multiple services independently subscribe to this event and react: Payment Service charges the customer, Inventory Service reserves stock, Shipping Service arranges logistics, Notification Service sends confirmation emails. Each service is decoupled. If Notification Service goes down, orders still get confirmed. New services can be added later without changing the Order aggregate.

Core Concepts

Domain Events Decouple Services

What Makes a Good Domain Event:

  • Named in past tense: OrderConfirmed, PaymentProcessed, CustomerOnboarded (not "ConfirmOrder")
  • Domain-significant: Represents something the business cares about
  • Immutable: Once created, never changes
  • Self-contained: Contains all data needed by subscribers
  • Versioned: Includes version number for evolution

Practical Example

from dataclasses import dataclass
from datetime import datetime
from uuid import UUID

@dataclass(frozen=True) # Immutable
class OrderConfirmed:
"""Domain Event: Order was confirmed and payment authorized.

Published when a customer's order is confirmed.
Multiple services react to this event independently.
"""
event_id: UUID
event_version: int = 1 # For future evolution
occurred_at: datetime = None

# Domain data
order_id: UUID
customer_id: UUID
order_total: float
currency: str = "USD"

def __post_init__(self):
if self.occurred_at is None:
object.__setattr__(self, 'occurred_at', datetime.utcnow())

@dataclass(frozen=True)
class OrderShipped:
"""Domain Event: Order was shipped and is in transit."""
event_id: UUID
event_version: int = 1
occurred_at: datetime = None

order_id: UUID
carrier_name: str
tracking_number: str
estimated_delivery: datetime

def __post_init__(self):
if self.occurred_at is None:
object.__setattr__(self, 'occurred_at', datetime.utcnow())

@dataclass(frozen=True)
class OrderCancelled:
"""Domain Event: Order was cancelled before shipment."""
event_id: UUID
event_version: int = 1
occurred_at: datetime = None

order_id: UUID
reason: str
refund_amount: float

def __post_init__(self):
if self.occurred_at is None:
object.__setattr__(self, 'occurred_at', datetime.utcnow())

Event-Driven Order Flow

Timeline:
--------

T0: Customer clicks "Confirm Order"
└─ POST /orders/{id}/confirm
└─ OrderService.confirm_order()

T0+10ms: OrderConfirmed event published
├─ PaymentService (subscribes)
├─ InventoryService (subscribes)
├─ ShippingService (subscribes)
└─ NotificationService (subscribes)

T0+50ms: Payment authorized (PaymentService processes)
└─ Emits PaymentProcessed event

T0+100ms: Inventory reserved (InventoryService processes)
└─ Emits InventoryReserved event

T0+150ms: Shipping arranged (ShippingService processes)
└─ Emits ShippingArranged event

T0+200ms: Confirmation email sent (NotificationService processes)
└─ Response to user: "Order confirmed"

Key properties:
- Order confirmed immediately (not waiting for all side effects)
- Each service handles its responsibility independently
- If a service fails, doesn't block order confirmation
- New services can be added by subscribing to OrderConfirmed

Patterns and Pitfalls

Patterns and Pitfalls

Emitting infrastructure-level events like UserLoggedIn, RowInserted, DatabaseConnected. These are not domain events—they don't express business significance.

Fix: Emit only business-meaningful events: OrderConfirmed, PaymentProcessed, CustomerOnboarded. Events should make sense to domain experts.

Subscribing to events and handling them synchronously, blocking the original operation. Creates tight temporal coupling and slow responses.

Fix: Publish events asynchronously to a message queue. Handlers consume and process independently. Let the aggregate operation complete immediately.

Event handlers that call external APIs or modify shared state. If handler fails, inconsistency results.

Fix: Handlers should be idempotent. Same event processed twice should produce same result. Handle retries gracefully.

Emitting events for every small change: ItemAdded, PriceChanged, TaxCalculated. Creates noise and makes workflows hard to follow.

Fix: Emit events only for business-significant changes. ItemAdded is fine if significant. TaxCalculated is probably too fine-grained.

Store events as the immutable source of truth. Aggregate state is derived by replaying events. Provides complete audit trail and temporal queries.

How to Use: Event store = source of truth. Snapshots = performance optimization for large event logs. Replay from snapshots to reconstruct state.

Saga is a process that listens to events, makes decisions, and emits commands to coordinate changes across multiple aggregates. Useful for workflows that span multiple aggregates.

How to Use: OrderSaga listens to PaymentFailed event, emits CancelOrder command. Orchestrates multi-aggregate workflows without tight coupling.

Include version in events to support evolution. Old versions can be migrated to new formats. Consumers can handle multiple versions.

How to Use: OrderConfirmed.version = 2. Include upconversion logic to handle old event format.

Messages that fail processing are sent to a dead letter queue for investigation and manual handling.

How to Use: Failed event processing goes to DLQ. Monitor DLQ. Investigate and replay when fixed.

Design Review Checklist

  • Are events named using past tense (Confirmed, Processed, Delivered)?
  • Do events represent domain-significant business occurrences?
  • Are events immutable (frozen/final classes)?
  • Do aggregates emit events when state changes significantly?
  • Can event handlers be added without modifying aggregates?
  • Are event handlers idempotent (same event processed twice = same result)?
  • Are events consumed asynchronously via message queue?
  • Can events be processed out of order without breaking invariants?
  • Is the event bus/message broker tested?
  • Can you trace important business workflows through events?
  • Are events versioned for long-term evolution?
  • Is there a strategy for handling failed event processing?

Self-Check

  1. What makes a good domain event? Records something important that happened in the business. Other parts of the system care about it. Expressed in ubiquitous language (not technical jargon). Examples: OrderConfirmed, PaymentProcessed, InventoryReserved.

  2. Should event handlers process sequentially or in parallel? Handlers can process in parallel if they don't depend on each other. If handler B depends on handler A completing, use Saga pattern to coordinate. Generally, prefer parallel processing with eventual consistency.

  3. How do you handle event versioning? Include version field in event. Subscribers can handle multiple versions. Upconvert old versions to new format. Example: OrderConfirmed.version = 2 supports both old and new subscribers.

  4. What if an event handler fails? Retry with exponential backoff. If still fails after retries, send to dead letter queue. Monitor DLQ. Investigate and replay when fixed. Handler should be idempotent so retries are safe.

  5. When should you use Event Sourcing? When you need complete audit trail, temporal queries ("what was the inventory on this date?"), or debugging ("replay the order process to find the bug"). Not all systems need event sourcing—use when value is clear.

ℹ️

One Takeaway: Domain Events decouple aggregates and services, enable event sourcing, and make workflows explicit. Emit events for important business occurrences in past tense. Let handlers react independently and asynchronously. This creates resilient, scalable, and auditable systems.

Next Steps

  1. Event Sourcing: Dive deeper into storing events as source of truth
  2. Sagas: Learn long-running transaction coordination with events
  3. Message Brokers: Understand Kafka, RabbitMQ, and other event platforms
  4. CQRS: See how events enable read model updates in CQRS patterns

References