Home
Backend from First Principles / Module 29 — Event-Driven Architecture

Event-Driven Architecture

Events vs commands, pub/sub patterns, Kafka, event sourcing — decoupling services with messages.


Commands vs Events — A Crucial Distinction

Most backend code is built around commands: "do this thing." Events flip that around.

Snippet
A command is a directive sent TO a specific recipient. It expects work to be done.
  CreateOrder(items, userId) → POST /orders → Order Service creates the order
Snippet
An event is a notification that something HAPPENED, broadcast to anyone interested.
  OrderCreated(orderId, userId, total) → published to a topic → 0 or N subscribers react

The mental shift:

Command thinking: "I need to send an email when an order is placed. Order service calls Email service."

Event thinking: "When an order is placed, I emit an OrderCreated event. Email service subscribes to that event and decides what to do with it. Inventory service also subscribes. Analytics also subscribes. Order service doesn't know any of them exist."

Why this matters: in a command world, the order service is coupled to every system that cares about orders. Add a new service that needs to know about orders? You modify the order service. In an event world, the order service emits one event and walks away. Adding a new consumer doesn't touch the producer.


Pub/Sub — The Plumbing

Pub/Sub (publish/subscribe) is the messaging pattern that makes events practical.

Text
                            ┌──────────────────┐
                            │  Message Broker  │
                            │   (the topic)    │
                            └────────┬─────────┘
                                     │
       ┌─────────────────────────────┼─────────────────────────────┐
       │                             │                             │
       ▼                             ▼                             ▼
  ┌────────────┐              ┌────────────┐              ┌────────────┐
  │ Subscriber │              │ Subscriber │              │ Subscriber │
  │   Email    │              │ Inventory  │              │ Analytics  │
  └────────────┘              └────────────┘              └────────────┘

Producer publishes ONCE. The broker fans out to all subscribers.
Producer doesn't know who's subscribed. Subscribers don't know each other.

Subscribers can come and go. New ones can join later. Existing ones can be removed without touching the producer.

Common pub/sub systems:

Pub/Sub vs Queues (covered in Module 13):
• A queue has ONE consumer per message — workers compete for jobs.
• A topic has MANY consumers per message — every subscriber gets every event.

Most modern systems use both. Use queues for "do this work." Use topics for "this happened."


Kafka — The Industry Standard

Kafka is what most large-scale event-driven systems run on. It's worth understanding even if you never operate it yourself.

What makes Kafka different:

1. The log is the truth. Kafka stores events in append-only logs called topics, partitioned for parallelism. Events are never deleted on consumption — they sit there for the configured retention period (often days or weeks).

2. Consumers track their own position. Each consumer group has an offset — "I've processed up to event #45,231." If a consumer crashes, it resumes from its last offset. If you add a new consumer, you can have it start from the beginning of the log.

Snippet
3. Replayability. Because the log persists, you can:
   • Rewind a consumer to last week and replay all events
   • Add a new service today and have it process every event ever published
   • Recover from a bug by reprocessing events with fixed code

4. High throughput. A single Kafka cluster can handle millions of events per second.

Text
Topic: orders   (partitioned for parallelism)
Partition 0:  [OrderCreated#1] [OrderShipped#1] [OrderCreated#2] ...
Partition 1:  [OrderCreated#3] [OrderCreated#4] ...
Partition 2:  [OrderCancelled#1] [OrderCreated#5] ...

Consumer group "billing"     — at offset 4,823 in each partition
Consumer group "analytics"   — at offset 4,823 in each partition (independent)
Consumer group "fraud-check" — at offset 4,800 (slightly behind)

The mental model: Kafka isn't a queue. It's a *log that consumers read from at their own pace*. That makes it ideal for event-driven systems where many independent consumers need to react to the same events.


Choreography vs Orchestration

When you have a workflow that spans multiple services — say, processing an order — you can structure it two ways.

Orchestration: A central conductor service tells everyone what to do.

Text
                        OrderOrchestrator
                              │
              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
        InventoryService   PaymentService   ShippingService
        "reserve item"     "charge card"    "create shipment"

Pros: Clear flow, one place to see the whole process, easy to debug.
Cons: The orchestrator becomes a god object that knows about everyone.

Choreography: Each service reacts to events; no central conductor.

Text
   Order placed → emits OrderCreated event
                          │
        ┌─────────────────┼──────────────────┐
        ▼                 ▼                  ▼
  Inventory reserves  Payment charges  Shipping waits...
  → emits             → emits          (waits for both)
  ItemsReserved       PaymentApproved
                          │
                          ▼
                  Shipping sees both events
                  → creates shipment
                  → emits ShipmentCreated

Pros: No god object, services are loosely coupled, easy to add new reactions.
Cons: Hard to see the whole flow without distributed tracing, easy to build accidental cycles.

Rule of thumb:
• Simple workflows with 2-3 steps: orchestration is clearer.
• Complex workflows with many independent reactions: choreography scales better.
• Either way, instrument with distributed tracing — otherwise you can't debug it.


Event Sourcing — Your Database is the Event Log

Event sourcing takes the event-driven idea to the extreme: instead of storing state, store the events that produced the state.

Traditional database approach:

SQL
-- What we usually do
UPDATE accounts SET balance = 950 WHERE id = 1;
-- We see the new balance. The previous balance is gone.

Event-sourced approach:

Text
events:
  AccountCreated(id=1, owner="Alice")
  Deposited(id=1, amount=1000)
  Withdrawn(id=1, amount=50)
  Withdrawn(id=1, amount=100)
  Deposited(id=1, amount=100)

// Current balance is computed by replaying all events: 0 + 1000 - 50 - 100 + 100 = 950

What you gain:
• Complete audit history — every change is recorded
• Time travel — "what was the balance on January 5th at 3 PM?" Just replay up to that point.
• Bug recovery — if you discover a calculation bug, fix the code and replay events
• Multiple read models — derive different views (current balance, monthly summary, fraud features) from the same events

What you give up:
• Querying is harder — "all accounts with balance > 1000" requires processing events into a read model first
• Storage is bigger — you keep every change forever
• Refactoring events is painful — old events live forever and your code has to handle them

Event sourcing isn't for everything. It shines in financial systems, audit-critical workflows, and complex domain logic. For a typical CRUD app, it's overkill. But the underlying mindset — "store what happened, not just the current state" — is broadly useful even when you don't go all-in.


⁂ Back to all modules