Event-Driven Architecture: Choreography, Orchestration & Sourcing
Checkout hangs because payment is slow — and the order service is blocked waiting on a synchronous call. Event-driven architecture breaks that chain: a service publishes what happened, and interested services react independently.
The cost is eventual consistency, duplicate delivery, and debugging across async boundaries. This article maps choreography vs orchestration, sagas, the outbox pattern, and event sourcing — with one checkout event trace and the failure paths that make those patterns necessary.
Compare the storage responsibilities of Event Sourcing and CQRS. Then choose transport semantics with the Kafka, RabbitMQ, and SQS comparison.
Events vs Commands vs Queries
Not every message is the same. An event is a fact that already happened: OrderPlaced, PaymentProcessed, InventoryReserved — past tense, immutable, zero to many consumers. A command asks one receiver to do work: PlaceOrder, ProcessPayment — and expects success or failure. A query reads state without changing it.
Checkout event trace (happy path): Client → Order API saves a pending order → publishes order.placed → Inventory reserves stock → publishes inventory.reserved → Payment charges → publishes payment.processed → Notification emails the customer. Order never imports Inventory or Payment clients; it only emits facts. Use commands inside a bounded context; publish domain events across service boundaries.
Quick reference
- Event: past-tense fact; broadcast; publishers do not know consumers.
- Command: imperative; one intended receiver; success/failure response.
- Query: read-only; no side effects.
- Domain event: meaningful to the business (OrderPlaced), not DatabaseRowInserted.
- Integration event: shared across services, usually via a broker.
- CloudEvents-style envelope: source, type, id, time, data — aids routing and dedupe.
Remember this
Publish events for cross-service communication; keep commands inside a bounded context where coupling is acceptable.
Choreography vs Orchestration
Two coordination styles. Choreography: each service reacts to events and publishes the next — no central controller; the workflow emerges from the chain. Orchestration: a saga coordinator or workflow engine tells each service what to do and collects results.
Choreography scales and has no single controller SPOF, but the full workflow lives across N services and N topics. Orchestration is visible in one place, but the orchestrator becomes a coupling and failure point. Start with choreography for short chains; add orchestration when rollback and visibility outgrow tribal knowledge.
Quick reference
- Choreography: loose coupling, hard to visualize the full workflow.
- Orchestration: visible workflow; orchestrator is a coupling point.
- Orchestration tools: Temporal, AWS Step Functions, Azure Durable Functions, Conductor.
- Escalate from choreography when chains exceed ~3 steps or need complex rollback.
- Both styles require idempotent consumers — brokers deliver at-least-once.
Remember this
Start with choreography for simple chains; add orchestration when you need one place to see and compensate multi-step work.
The Saga Pattern for Distributed Transactions
Two-phase commit holds locks across services and fails awkwardly when a coordinator crashes. A saga is a sequence of local transactions; if a step fails, compensating transactions undo prior steps. Example: reserve inventory → charge payment → confirm. If payment fails, release the reservation and notify the customer.
Failure path: Payment returns card_declined → Payment publishes payment.failed → Inventory compensates with inventory.released → Order marks cancelled. Sagas give eventual consistency, not ACID isolation — concurrent sagas can see intermediate state.
Quick reference
- Each step is a local transaction; cross-service consistency is eventual.
- Compensation is domain logic — not always a simple undo.
- Choreography saga: success/failure events drive next step or compensate.
- Orchestration saga: coordinator tracks state and dispatches commands.
- Pivot step: after which compensation is impossible (e.g. goods shipped) — design explicitly.
- Idempotency keys on every event are mandatory under retries.
Remember this
Sagas replace distributed locks with local transactions plus compensations — accept eventual consistency for availability.
The Outbox Pattern: Reliable Event Publishing
Save-then-publish is not atomic: crash after DB commit and the event never leaves; publish-then-save and consumers act on work that rolled back. The outbox writes the business row and the outgoing event in one transaction; a relay publishes to the broker and marks rows sent.
When not to skip it: any path where lost or duplicate-wrong events corrupt money, inventory, or compliance. Polling relays and CDC (e.g. Debezium on Postgres WAL) both work; consumers must still dedupe by eventId.
Quick reference
- Relay is at-least-once — consumers must be idempotent.
- Poll the outbox table or use CDC on the WAL to avoid busy polling.
- Put a unique event id in the payload for consumer dedupe.
- Delete or archive old outbox rows after confirmed delivery.
- Standard companion to CQRS and event sourcing write paths.
Remember this
Commit business state and the outbox row atomically; publish to the broker separately with retries and consumer deduplication.
Event Sourcing
Traditional persistence stores current state. Event sourcing stores every change as an immutable event; current state is a projection of the stream. An account balance is the fold of deposits and withdrawals, not a single mutable column.
You gain an audit log and point-in-time reconstruction. You pay with projection complexity, eventual consistency, and snapshots once streams grow. When not to use: simple CRUD, heavy ad-hoc reporting without a separate read model, or a team that cannot operate eventual consistency. Prefer a normal table plus outbox unless you need the audit/time-travel properties themselves.
Quick reference
- Event store: append-only log (EventStoreDB, Marten on Postgres, etc.).
- Projection: read model rebuilt or updated from events.
- Snapshot: checkpoint aggregate state so replay does not start at event 1.
- Pairs naturally with CQRS (write = events, read = projections).
- Use when: audit, finance-like domains, time-travel debugging.
- Skip when: CRUD apps, teams new to eventual consistency, query-first workloads.
Remember this
Event sourcing earns its complexity only when you need the audit trail or reconstruction — not as a default microservice style.
Key takeaway
Event-driven architecture trades sync simplicity for decoupling and resilience. Outbox makes publish reliable; sagas make partial failure recoverable; choreography keeps short chains simple; orchestration and event sourcing are opt-in when visibility or audit demand them.
Practice (25 min): Trace one checkout: list every event name, producer, and consumer. Inject a payment failure after inventory reserved — write the compensating handler and prove a duplicate payment.failed delivery is a no-op. Confirm the order ends cancelled and inventory is free.
Related Articles
Explore this topic