01

Requirements & scope

Problem Statement & Requirements

Design an event-driven architecture platform that enables loosely-coupled microservices to communicate asynchronously through events, supports event sourcing for audit/replay, provides schema governance, and handles the operational challenges of distributed event systems at scale — the architectural backbone behind systems at Netflix, Uber, LinkedIn, and Shopify.

Functional Requirements

  • Event bus / broker — publish-subscribe messaging backbone (Kafka-based) with topics, partitions, and consumer groups
  • Event schema registry — centralized schema management with versioning, compatibility checks, and serialization (Avro/Protobuf)
  • Event catalog / discovery — searchable catalog of all event types across the organization, their schemas, owners, and consumers
  • Event sourcing — store state as a sequence of immutable events; reconstruct current state by replaying events
  • CQRS (Command Query Responsibility Segregation) — separate write models (commands → events) from read models (materialized views)
  • Saga orchestration — coordinate multi-service transactions via event-driven sagas with compensation
  • Dead letter queue (DLQ) — capture failed events for inspection, replay, and manual resolution
  • Event replay — replay historical events to rebuild state, backfill new consumers, or debug issues
  • Exactly-once semantics — idempotent event processing to prevent duplicate side effects
  • CDC (Change Data Capture) — stream database changes as events (Debezium pattern)

Non-Functional Requirements

  • High throughput — handle 5M+ events/second across the platform
  • Low latency — event publish-to-consume in < 50 ms (p99)
  • Durability — zero event loss once acknowledged
  • Ordering — guaranteed ordering within a partition/entity
  • Schema evolution — producers and consumers evolve independently without breaking each other
  • Observability — trace events across services (distributed tracing), monitor consumer lag, detect broken consumers
  • Multi-team governance — hundreds of teams publish/consume events; need ownership, discovery, and compatibility rules

Out of Scope

  • Full message broker internals (see Distributed Message Queue design)
  • Specific business domain modeling (DDD bounded contexts)
  • Stream processing engine internals (Kafka Streams / Flink)
02

Scale estimations

Scale Estimations

Traffic

Metric Value
Total events produced / second 5M
Total events consumed / second 25M (avg 5 consumers per event)
Distinct event types 5,000
Event topics 2,000
Total partitions 50,000
Producer services 500
Consumer services 2,000
Consumer groups 3,000
Saga workflows active concurrently 500K

Storage

Metric Value
Average event size 1 KB
Events per day 5M/s × 86,400 = ~432B
Raw event storage per day 432B × 1 KB = ~432 TB/day
Event retention (hot, 7 days) ~3 PB
Event retention (warm/archive, 1 year) ~50 PB (compressed)
Event store (sourced aggregates) ~10 TB
Schema registry ~50 MB (5K schemas × 10 versions × 1 KB)
Saga state store 500K × 5 KB = ~2.5 GB

Bandwidth

Metric Value
Producer ingress (5M/s × 1 KB) ~5 GB/s
Consumer egress (25M/s × 1 KB) ~25 GB/s
Cross-DC replication ~5 GB/s
Total cluster bandwidth ~35 GB/s

Hardware Estimate

Component Spec
Kafka brokers 100-200 (NVMe SSDs, 25 Gbps NIC)
Schema registry 3 nodes (HA, lightweight)
Event store DB (sourcing) 20-50 shards
CQRS read model stores 50-100 instances (varies by model)
Saga orchestrator 10-20 nodes
CDC connectors (Debezium) 30-50 (one per source DB)
Event router / mesh 20-50 nodes
03

Layered architecture

High-Level Architecture

High-Level ArchitectureExcalidraw diagram · editable shapes · reveal step by stepExplore

Component Breakdown

Component BreakdownExcalidraw diagram · editable shapes · reveal step by stepExplore
04

API & contracts

Event Design & Schema

Event Envelope (Standard Structure)

json
{
  "event_id": "evt-7f3a9b2c-1234-5678-abcd-ef0123456789",
  "event_type": "orders.placed",
  "event_version": "3.1",
  "source": "order-service",
  "timestamp": "2026-04-03T10:00:00.123Z",
  "correlation_id": "req-abc123",
  "causation_id": "evt-previous-xyz",
  "partition_key": "order-789",
  "tenant_id": "acme-corp",
  "metadata": {
    "user_id": "user-alice",
    "trace_id": "trace-456",
    "span_id": "span-789"
  },
  "data": {
    "order_id": "order-789",
    "customer_id": "cust-123",
    "items": [
      {"sku": "SKU-001", "quantity": 2, "unit_price": 2999}
    ],
    "total_amount": 5998,
    "currency": "usd",
    "shipping_address": {
      "city": "New York",
      "country": "US"
    }
  }
}

Event Naming Conventions

Event Naming ConventionsExcalidraw diagram · editable shapes · reveal step by stepExplore

Schema Registry & Evolution

Schema Registry & EvolutionExcalidraw diagram · editable shapes · reveal step by stepExplore
05

Data model

Workshop note · added for the website’s common reading format

Identify the authoritative records behind Schema registry, Read projections, Saga coordinator. For each record, define its identity, access pattern, partition key, retention, and version. Separate durable truth from rebuildable indexes and caches.

Trace the most important read and write through the request-flow and core-decision sections before choosing a schema. Avoid adding a distributed transaction unless the invariant truly requires it.

06

Core design decisions

Workshop note · added for the website’s common reading format

Use the detailed source walkthroughs in this article to examine Event-Driven Architecture at this layer. State the requirement, propose the smallest component that satisfies it, and make its cost and failure mode explicit.

07

Request flows

Workshop note · added for the website’s common reading format

  1. Producer service: Define events as immutable facts about a completed business change.
  2. Outbox + CDC: Commit business data and an outbox record together, then relay committed events to the bus.
  3. Event bus: Choose keys for required ordering and retain enough history for consumers to recover.
  4. Consumer services: Expect redelivery and make business side effects idempotent rather than assuming universal exactly-once delivery.
  5. Schema registry: Version event contracts and validate backward or forward compatibility before release.
  6. Read projections: Build query-specific read models from events and expose their expected consistency lag.
08

Performance & caching

Workshop note · added for the website’s common reading format

Start with the dominant access pattern of Event-Driven Architecture. Schema registry is one place to inspect capacity and tail latency. Measure before introducing a cache: define the cache key, invalidation policy, stale-data budget, and cold-start behavior.

Batch independent work where the latency budget allows it. Bound queues and concurrency, and verify that an optimization does not move the bottleneck to a dependency.

09

Advanced design

Event Sourcing Deep Dive

Concept

ConceptExcalidraw diagram · editable shapes · reveal step by stepExplore

Snapshots (Performance Optimization)

Snapshots (Performance Optimization)Excalidraw diagram · editable shapes · reveal step by stepExplore

CQRS (Command Query Responsibility Segregation)

CQRS (Command Query Responsibility Segregation)Excalidraw diagram · editable shapes · reveal step by stepExplore

Saga Pattern — Distributed Transactions

The Problem

The ProblemExcalidraw diagram · editable shapes · reveal step by stepExplore

Choreography-Based Saga (No Central Coordinator)

Choreography-Based Saga (No Central Coordinator)Excalidraw diagram · editable shapes · reveal step by stepExplore

Orchestration-Based Saga (Central Coordinator)

Orchestration-Based Saga (Central Coordinator)Excalidraw diagram · editable shapes · reveal step by stepExplore

Change Data Capture (CDC)

Change Data Capture (CDC)Excalidraw diagram · editable shapes · reveal step by stepExplore
10

Edge cases

Handling Edge Cases

Event Ordering Across Services

EXAMPLE
Problem: OrderPlaced and PaymentCompleted arrive out of order

  Event 1: OrderPlaced (published at T=0)
  Event 2: PaymentCompleted (published at T=1)

  Due to partitioning / network:
  Consumer sees: PaymentCompleted BEFORE OrderPlaced

Solution:
  1. Partition by entity ID (order_id)
     → All events for one order go to same partition
     → Within partition: strict ordering guaranteed
     ✅ Handles 99% of ordering needs

  2. For cross-entity ordering (rare):
     → Causation chain: each event carries causation_id
     → Consumer buffers events, processes in causal order
     → Or: accept eventual consistency (out-of-order OK for analytics)

  3. For strict global ordering (very rare):
     → Single partition (kills parallelism — avoid if possible)
     → Or: Lamport timestamps for causal ordering

Duplicate Events (Exactly-Once Processing)

EXAMPLE
Problem: Kafka guarantees at-least-once delivery
  → Consumer may see the same event twice (rebalance, retry)

Solutions:
  Layer 1: Idempotent Processing
    Each event has unique event_id
    Consumer tracks processed event_ids in local store
    Before processing: check "have I seen event_id X?"
    → Yes: skip (already processed)
    → No: process + record event_id

  Layer 2: Transactional Outbox (for producers)
    Instead of publishing directly to Kafka:
    1. Write event to outbox table in SAME DB transaction as state change
    2. Background poller reads outbox → publishes to Kafka → marks as published
    3. If service crashes after DB commit but before Kafka publish:
       → Poller retries → event eventually published
    4. If service crashes before DB commit:
       → Nothing in outbox → no duplicate → safe

  Layer 3: Kafka Transactions (for Kafka-to-Kafka processing)
    Read from input topic + write to output topic + commit offset = ATOMIC
    Kafka idempotent producer + transactional consumer = exactly-once

  Recommendation: Idempotent consumers (Layer 1) for all services
    Transactional outbox (Layer 2) for critical producers

Schema Breaking Change

EXAMPLE
Problem: Producer adds a required field → all consumers break

Prevention:
  1. Schema Registry enforces FULL compatibility
     → Reject schema that removes fields or adds required fields
     → Only optional field additions allowed

  2. If breaking change is truly needed:
     → Create NEW topic: orders.placed.v2
     → Run both v1 and v2 in parallel (migration period)
     → Consumers migrate to v2 at their own pace
     → After all consumers migrated → deprecate v1 topic
     → Timeline: 2-4 weeks migration window typical

  3. Event versioning in schema:
     → event_version field in envelope
     → Consumer checks version → delegates to appropriate handler
     → v1 handler and v2 handler coexist in consumer code

Consumer Falls Behind (Lag)

EXAMPLE
Problem: Analytics consumer processes at 50K events/sec
  Producer writes 100K events/sec → lag grows unboundedly

Detection:
  Monitor: consumer_lag = latest_offset - committed_offset
  Alert: lag > 1M events OR lag growing for > 10 minutes

Response:
  1. Scale out consumers (add instances to consumer group)
     → Kafka rebalances partitions across more consumers
     → Max parallelism = number of partitions

  2. If at max consumers (= partition count):
     → Optimize processing (batch DB writes, reduce per-event cost)
     → Or: add more partitions (requires topic reconfiguration)

  3. For non-critical consumers (analytics):
     → Accept lag (process at own pace, catch up during off-peak)
     → Skip old events if > 24h behind (stale data less useful)

  4. For critical consumers (payment):
     → Auto-scale aggressively
     → Alert if lag > 1 minute
     → This consumer should NEVER fall behind
11

Tradeoffs

Tradeoffs & Design Decisions Summary

Decision Option A Option B Chosen Why
Event format JSON Avro/Protobuf Avro Schema enforcement, compact, evolution rules; JSON for debugging/external APIs
Saga pattern Choreography (decentralized) Orchestration (coordinator) Orchestration for complex, choreography for simple Orchestrator provides visibility + central control; choreography for 2-3 step simple flows
Event sourcing All domains Select domains Select (orders, payments, audit-critical) Not all domains benefit; CRUD simpler for user profiles, configs
Ordering Global (single partition) Per-entity (partition by ID) Per-entity Global ordering kills parallelism; per-entity sufficient for 99% of use cases
Duplicate prevention Kafka exactly-once only Idempotent consumers + outbox Idempotent consumers + outbox Defense in depth; idempotent consumers protect against ALL duplicate sources
Schema compatibility NONE (no checks) FULL (backward + forward) FULL Prevents breaking changes; producers and consumers evolve independently
CDC tool Application-level dual-write Log-based CDC (Debezium) Debezium No app code changes; captures ALL changes; ordered; low overhead
Event retention Short (24h) Long (7 days + archive) 7 days hot + 1 year archive Replay capability; new consumer backfill; debugging; regulatory compliance
CQRS read models Same DB as write Separate optimized stores Separate Each query pattern gets optimal storage (ES for search, ClickHouse for analytics)
DLQ Skip and log DLQ topic + alerting DLQ + alert Never silently drop events; DLQ enables inspection and replay
12

Reliability & fault tolerance

Dead Letter Queue & Error Handling

Dead Letter Queue & Error HandlingExcalidraw diagram · editable shapes · reveal step by stepExplore

Observability for Event-Driven Systems

Observability for Event-Driven SystemsExcalidraw diagram · editable shapes · reveal step by stepExplore
13

Production architecture

Full System Architecture (Production-Grade)

Full System Architecture (Production-Grade)Excalidraw diagram · editable shapes · reveal step by stepExplore
14

Further exploration

When to Use (and NOT Use) Event-Driven Architecture

When to Use (and NOT Use) Event-Driven ArchitectureExcalidraw diagram · editable shapes · reveal step by stepExplore
15

Interview playbook

Interview Tips

  1. Start with WHY events — "Event-driven architecture decouples producers from consumers. The Order Service publishes OrderPlaced without knowing or caring who consumes it. Payment, Inventory, Notifications, Analytics all react independently. Adding a new consumer requires zero changes to the producer."

  2. Events vs Commands is a fundamental distinction — Events: past tense, broadcast, immutable facts ("OrderPlaced"). Commands: imperative, directed, one target ("ShipOrder"). Events describe what happened; commands describe what should happen. Mixing them up creates tight coupling.

  3. Schema Registry prevents breaking changes — "Every event schema is registered with FULL compatibility enforcement. Adding an optional field = safe. Removing a field = rejected by registry. Producers and consumers evolve at their own pace. This is what prevents 'I deployed and broke 10 downstream services.'"

  4. Saga pattern replaces distributed transactions — "2PC locks across 4 databases = fragile and slow. Saga coordinates via events with compensating actions. Payment fails after inventory reserved? Publish InventoryReleaseRequested. Each step is undoable. Orchestration-based for complex flows, choreography for simple 2-3 step flows."

  5. Event Sourcing is powerful but use selectively — Don't event-source everything. "Order management, payment transactions, audit-critical domains = event sourcing (full history, time-travel, replay). User profiles, configuration = CRUD (simpler, sufficient). Event sourcing adds complexity; use it where the audit/replay value justifies it."

  6. CQRS enables optimal read models — "Write model normalized for consistency. Read models denormalized for query performance. Order dashboard in PostgreSQL, search in Elasticsearch, analytics in ClickHouse — each optimized for its query pattern. All built from the same event stream."

  7. CDC bridges legacy systems — "Can't modify the legacy monolith to publish events? Debezium reads the PostgreSQL WAL and publishes every INSERT/UPDATE/DELETE as an event to Kafka. Zero code changes. This is how most companies start their event-driven migration."

  8. DLQ is non-negotiable — "Events that fail processing after N retries go to a Dead Letter Queue. Never silently drop events. DLQ enables inspection ('why did this fail?'), replay ('fix the bug, replay events'), and alerting ('DLQ depth > 0 = someone needs to look')."

  9. Observability is the hardest part — "A synchronous REST call chain is easy to trace. An async event flow across 5 services via Kafka is not. Propagate correlation_id + trace_id through every event. Build an event topology dashboard showing which services produce/consume which topics. Monitor consumer lag religiously."

  10. End with the honest tradeoff — "Events give you loose coupling, independent scaling, and audit trails. They cost you eventual consistency, debugging complexity, and operational overhead. The right question isn't 'should we use events?' — it's 'which interactions benefit from decoupling enough to justify the complexity?'"