AnyLearn
All lessons
Computer Scienceintermediate

Event-Driven Architecture

Commands tell, events announce. How event-driven systems decouple producers from consumers, when CQRS and event sourcing earn their complexity, and the eventual-consistency tax you pay either way.

Not signed in โ€” your progress and quiz score won't be saved.
Lesson progress1 / 8

Commands vs events: a small but important distinction

Both are messages, but they carry different intent:

  • A command says "do this". It's directed at a specific handler. Imperative. May fail or be rejected. Example: ChargeCard(orderId=42, amount=99.99).
  • An event says "this happened". It's broadcast to anyone interested. Past tense. Immutable. Example: OrderPlaced(orderId=42, amount=99.99, userId=7).

The difference matters. Commands are 1-to-1; events are 1-to-many. A new consumer can subscribe to an event without the producer ever knowing. That's the structural property that makes event-driven systems extensible โ€” and the same property that makes them harder to reason about.

Full lesson text

All 8 steps on one page โ€” for reading, reference, and search.

Show

1. Commands vs events: a small but important distinction

Both are messages, but they carry different intent:

  • A command says "do this". It's directed at a specific handler. Imperative. May fail or be rejected. Example: ChargeCard(orderId=42, amount=99.99).
  • An event says "this happened". It's broadcast to anyone interested. Past tense. Immutable. Example: OrderPlaced(orderId=42, amount=99.99, userId=7).

The difference matters. Commands are 1-to-1; events are 1-to-many. A new consumer can subscribe to an event without the producer ever knowing. That's the structural property that makes event-driven systems extensible โ€” and the same property that makes them harder to reason about.

2. Why teams reach for EDA

What event-driven architecture gives you, in order of value:

  • Decoupling: the order service emits OrderPlaced; the email, inventory, and analytics services react. The order service doesn't import any of them.
  • Extensibility: a new team wants to react to orders โ†’ they subscribe; no change to the producer.
  • Async load-shedding: heavy work moves off the request path. Users get fast "accepted" responses; processing happens in the background.
  • Auditability: a stream of past events is itself an audit log of every meaningful state change.

What it costs: every service-to-service interaction becomes async, which forces eventual consistency on the product side. "I just placed an order; why isn't it in my order history yet?" is the daily UX cost.

3. Choreography vs orchestration

Two ways to coordinate multi-step business processes:

  • Choreography: each service listens for events and reacts. There's no central coordinator. "OrderPlaced โ†’ InventoryReserved โ†’ PaymentRequested โ†’ PaymentCharged โ†’ OrderConfirmed". Each transition is owned by the service that performs it.
  • Orchestration: a central service (often a workflow engine like Temporal, Step Functions, or Airflow) tells each service what to do, in order. The orchestrator owns the process state.

Choreography is simpler to bootstrap but harder to reason about โ€” "what's the full flow?" requires reading every service. Orchestration centralises the flow but makes the orchestrator a critical dependency. Most teams use orchestration for explicit business processes ("checkout") and choreography for cross-cutting reactions ("send a welcome email when a user signs up").

4. An event-driven order flow (choreographed)

Each service reacts to the event that matters to it.

flowchart LR
  Order["OrderPlaced event"] --> Inv["Inventory: reserve stock"]
  Order --> Email["Email: order confirmation"]
  Order --> Pay["Payment: charge card"]
  Pay --> Paid["OrderPaid event"]
  Paid --> Ship["Shipping: schedule delivery"]
  Paid --> Analytics["Analytics: record revenue"]

5. Event sourcing: events as source of truth

Conventional design stores current state. Event sourcing flips this: store every event that ever happened, derive current state by replaying them.

Example, for a bank account:

Event log:
  AccountOpened(id=1, owner=Alice)
  Deposited(id=1, amount=100)
  Withdrew(id=1, amount=30)
  Deposited(id=1, amount=50)

Current state (derived): balance = 120

Upsides: total auditability, easy time-travel debugging ("what was the balance last Tuesday?"), ability to add new projections without rewriting history.

Downsides: replays get slow as the log grows (mitigated with snapshots), schema changes to events are painful, every query against current state is a derivation. Use it when audit/history is genuinely a first-class requirement (banking, healthcare, regulated industries). Don't use it because it sounds elegant.

6. CQRS: separate read and write models

Command Query Responsibility Segregation says: the model you use to write doesn't have to match the model you use to read.

  • Write model: optimised for command validation. Normalised, transactional, often event-sourced.
  • Read model: optimised for queries. Denormalised, pre-aggregated, possibly in a different store (Elasticsearch, a cache, a wide-column DB).
  • The link: every write emits an event; the read model subscribes and updates.

CQRS is most useful when reads and writes have wildly different shapes โ€” e.g. you write "events" but read "a list of all orders with totals by category by month". The complexity is real (you now have to keep two stores in sync), so reserve it for cases where a single model genuinely fights the workload.

7. Eventual consistency leaks into the UI

Event-driven systems are eventually consistent across services. The user-visible symptoms:

  • Place an order; it's not in your list yet.
  • Update your profile; some part of the app still shows the old version.
  • Like a post; the count doesn't update for 200ms.

Mitigations:

  • Optimistic UI: the client updates immediately, then reconciles. The user sees instant feedback; the system catches up.
  • Read-your-writes within a single service: the writer's own service should be strongly consistent even if cross-service is eventual.
  • Versioned reads: client passes a "min version" with reads; server waits briefly if the read model is behind.
  • Be honest in the UX: "processing your order" pages with a spinner are honest; pretending success then surprising the user with a backend failure 5 seconds later is not.

Most EDA UX bugs come from teams treating async like sync. Plan for the gap.

8. When NOT to go event-driven

EDA is overkill (or actively harmful) when:

  • Strong consistency is mandatory: you can't tolerate "the email said your card was charged but the order isn't there yet". Some financial flows fall here; use a synchronous transaction instead.
  • Single team, single service: the decoupling buys you nothing. The async complexity is pure tax.
  • Tight request-response shapes: "user submits form, expects success/failure within 200ms". Async events don't fit.
  • Low data velocity: a system processing 100 events/day doesn't need Kafka. A cron job and a database table will do.

The decision to go event-driven should be driven by independent service evolution and fan-out needs, not by it being the fashionable architecture this year.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. What's the fundamental difference between a command and an event?
    • Commands are JSON, events are protobuf
    • Commands are imperative ("do this") and 1-to-1; events are past-tense ("this happened") and 1-to-many
    • Events are faster than commands
    • Commands require authentication; events don't
  2. Your team uses choreography but no-one can describe the full order-fulfillment flow without reading 5 services. What does this signal?
    • Add more services
    • Convert to orchestration for the explicit business process so flow control is centralised
    • Document better; nothing structural to change
    • Switch to monolith
  3. Which is the strongest signal that event sourcing is the right call?
    • You want to use Kafka
    • Audit trails, time-travel debugging, and the ability to reconstruct past states are first-class product requirements
    • Your data fits in a normalised schema
    • You have a small team
  4. A user places an order; the order doesn't appear in their order history for a second. The team is asked to fix it. Best mitigation?
    • Switch to a synchronous architecture
    • Optimistic UI: client shows the order immediately, reconciles with the server when the read model catches up
    • Lengthen the loading spinner
    • Email the user instead
  5. Which scenario is *least* well-suited to event-driven architecture?
    • A new analytics team needs to track every checkout without coordinating with the checkout team
    • A retry queue for background email sending
    • A user submits a login form and needs instant success/failure to redirect
    • Order status updates fanning out to email, SMS, and CRM

Related lessons