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)
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 |
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
API & contracts
Event Design & Schema
Event Envelope (Standard Structure)
{
"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
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.
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.
Request flows
Workshop note · added for the website’s common reading format
- Producer service: Define events as immutable facts about a completed business change.
- Outbox + CDC: Commit business data and an outbox record together, then relay committed events to the bus.
- Event bus: Choose keys for required ordering and retain enough history for consumers to recover.
- Consumer services: Expect redelivery and make business side effects idempotent rather than assuming universal exactly-once delivery.
- Schema registry: Version event contracts and validate backward or forward compatibility before release.
- Read projections: Build query-specific read models from events and expose their expected consistency lag.
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.
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
Edge cases
Handling Edge Cases
Event Ordering Across Services
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 orderingDuplicate Events (Exactly-Once Processing)
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 producersSchema Breaking Change
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 codeConsumer Falls Behind (Lag)
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 behindTradeoffs
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 |
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
Production architecture
Full System Architecture (Production-Grade)
Full System Architecture (Production-Grade)Excalidraw diagram · editable shapes · reveal step by stepExplore
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
Interview playbook
Interview Tips
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."
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.
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.'"
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."
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."
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."
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."
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')."
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."
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?'"