Behavioral Patterns in Event Systems
Behavioral Patterns in Event Systems composes Observer, Command, State, Mediator, and Chain of Responsibility into coherent event-driven architecture.
Introduction
Behavioral Patterns in Event Systems composes Observer, Command, State, Mediator, and Chain of Responsibility into coherent event-driven architecture. Production systems rarely use one pattern — Command carries intent in, Domain Events are facts out, State manages aggregate lifecycle, CoR validates pipelines, Mediator/Saga coordinates workflows.
The story
A logistics platform used Observer for everything — every change emitted events, every handler listened. After 18 months no one could trace state mutations. Reshaping around Commands (intent in), Domain Events (facts out), Saga Mediator per workflow, and explicit State on aggregates restored order and made on-call survivable.
The business problem
Observer-for-everything event systems become untraceable spaghetti:
- No intent trace: pure pub/sub — who started the flow?
- Circular cascades: handlers mutate each other in loops.
- Scattered workflow: long-running logic in random listeners.
- Hidden state machines: lifecycle rules buried in event handlers.
The problem teams faced
Compose behavioral patterns when:
- Event-driven architecture lacks clear command vs event distinction.
- Workflows span aggregates/services with no owning coordinator.
- Validation/enrichment duplicated or skipped at boundaries.
- Aggregate lifecycle rules live in subscribers instead of aggregate.
Understanding the topic
Intent: Assign each behavioral pattern a clear role in event systems.
- Command — intent inward (PlaceOrder).
- CoR pipeline — validate → authorize → enrich → audit.
- Aggregate + State — business rules; emits Domain Events.
- Domain Event — immutable fact (OrderPlaced).
- Observer/Subscriber — projections, notifications, BI.
- Mediator/Saga — coordinates next Command across workflow.
Internal architecture
Event-driven behavioral stack:
Command → [CoR: validate · authorize · audit]↓Order Aggregate (State machine)↓ emitsOrderPlaced (Domain Event)↓┌─────────┴──────────┐▼ ▼Projections Saga Mediator(Observer) → ReserveInventory Command→ on failure: CancelOrder Command
Visual explanation
Three diagrams map patterns to event flow:
Informative example
Before / after — Observer soup to composed patterns:
// ❌ Observer everything — untraceable cascadesevents.on("inventoryChanged", () => {orders.tryShip() // mutates orders})events.on("orderUpdated", () => {inventory.release() // circular cascade})// ✅ Composed behavioral patternsasync function handle(cmd: PlaceOrderCommand) {await pipeline.run(cmd) // CoR: validate · authconst events = orderAggregate.handle(cmd) // State + rulesawait bus.publish(events) // OrderPlaced fact}saga.on(OrderPlaced, async (e) => {await bus.send(new ReserveInventoryCommand(e.orderId)) // Mediator})subscribers.on(OrderPlaced, projection.update) // Observer — read only
Execution workflow
Command arrives
CoR pipeline validates, authorizes, enriches.
Real-world use
Event-sourced banking, e-commerce fulfillment, ride-hailing dispatch, Axon/EventStoreDB, Kafka + Temporal sagas, NestJS CQRS module.
Production case study
Logistics platform reshape:
- Scenario: Observer for everything; untraceable mutations after 18 months.
- Decision: Commands in, events out, Saga per workflow, State on aggregates.
- Outcome: On-call could trace flows; new reactions didn't edit publishers.
- Metric: incident MTTR down 40% with correlation IDs on command/event chain.
Trade-offs
- Pro: clear ownership per pattern role; traceable flows.
- Pro: aggregates enforce invariants; subscribers stay read-only.
- Con: more upfront architecture than "just emit events."
- Con: saga complexity — compensations, idempotency required.
Decision framework
- Command for intent; Domain Event for fact — always separate.
- State on aggregate for lifecycle — not in subscribers.
- Saga/Mediator when workflow spans steps/aggregates.
- Observer for reactions that don't command back synchronously in loop.
Best practices
- Correlation ID from command through all events and saga steps.
- Idempotent event handlers — at-least-once delivery.
- Subscribers don't send commands that loop to same aggregate synchronously.
- One saga owns each long-running workflow.
Anti-patterns to avoid
- Fat event handlers with business logic and state mutations.
- Choreography soup — 12 listeners each issuing next command.
- Command disguised as event ("PlaceOrderEvent" that triggers work).
Common mistakes
- Distributed saga without compensation — partial failure inconsistent state.
- Read model subscriber mutating write model.
- Missing outbox — lost events on crash after DB commit.
Debugging tips
- Trace correlation ID across command → events → saga commands.
- Event audit log: who published, who consumed, handler duration.
Optimization strategies
- Separate hot in-process observers from cold async bus subscribers.
- CoR pipeline cache for repeated validation on similar commands.
Common misconceptions
- Event-driven ≠ Observer only. Need Command, State, Saga too.
- CQRS combines Command + Observer projections.
- Choreography vs Saga Mediator — know when central coordination wins.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1IntermediateQuestionCommand vs Domain Event?+
Answer
Follow-up
2AdvancedQuestionWhere State machine lives?+
Answer
Follow-up
3AdvancedQuestionSaga vs Observer chain?+
Answer
Follow-up
4IntermediateQuestionCoR role in event systems?+
Answer
Follow-up
Summary
You can design event systems assigning each behavioral pattern a role and avoid Observer spaghetti. Tell the logistics 18-month reshape story — if you can do that, you own the Behavioral section.