Event-driven architecture (EDA) trades the simplicity of request-response APIs for a model where services communicate by publishing facts: "OrderPlaced," "PaymentCaptured," "InventoryReserved." Consumers react without the producer knowing who listens.
Teams adopt EDA when workflows sprawl across services, when peak load is spiky, or when you need to add features without rewiring every upstream caller. It also introduces new failure modes—duplicate events, ordering surprises, and debugging traces that hop across queues.
This guide covers what backend teams should know before committing.
Events vs commands vs queries
Clarity on message intent prevents architectural drift.
| Type | Meaning | Example |
|---|---|---|
| Event | Something happened (past tense) | OrderShipped |
| Command | Please do something (imperative) | ChargeCustomer |
| Query | What is the current state? | GetOrderStatus |
EDA centers on events: immutable notifications multiple subscribers can process at their own pace. Commands belong in synchronous APIs or dedicated task queues when you need a single handler and immediate acknowledgement.
Mixing "please do X" events with true domain events creates double-processing and unclear ownership.
Core building blocks
Event producers emit facts after a transaction commits (or after an outbox relay). Producers should not embed consumer logic.
Event broker routes messages: Kafka, Amazon SNS/SQS, Google Pub/Sub, RabbitMQ, Azure Event Hubs. Choice depends on throughput, ordering guarantees, replay needs, and ops maturity.
Consumers subscribe to topics or queues, process idempotently, and may emit downstream events.
Event schema registry (optional but valuable) stores versioned Avro/Protobuf/JSON schemas so producers and consumers evolve safely.
Observability — correlation IDs propagated through events, dead-letter queues (DLQs), and lag metrics per consumer group.
Typical flow
[Checkout API] --commits--> [Orders DB]
|
v (outbox or transactional emit)
[OrderPlaced event] --> [Broker]
| | |
v v v
[Email svc] [Analytics] [Warehouse svc]
The checkout API returns quickly. Fulfillment, emails, and analytics proceed asynchronously. Users perceive faster responses; the system absorbs downstream slowness—up to a point.
When EDA helps
Decoupling teams — Marketing wants a new "abandoned cart" email without the cart service knowing about SMTP. They subscribe to CartUpdated events.
Elastic scale — Image processing spikes during uploads. A queue buffers work; workers scale independently.
Audit and replay — Kafka-style logs let you rebuild analytics tables or reprocess after a bug fix.
Polyglot systems — JVM billing and Python ML scoring integrate through events without shared libraries.
When to stay request-response
Not every interaction deserves a bus.
- Simple CRUD with two services
- User-facing reads needing strong consistency on the same screen
- Low-volume internal admin tools
- Teams without operational muscle for broker uptime and consumer lag alerts
EDA adds moving parts. If you do not have on-call coverage for your message backbone, start with synchronous APIs and add events at clear pain points.
Design rules that survive production
Design events as facts, not commands
Good: PaymentFailed { orderId, reason, amount }
Risky: SendPaymentFailureEmail { to, template } — that couples payment domain to email templates.
Keep events domain-centric. Let consumers decide reactions.
Plan for at-least-once delivery
Most brokers guarantee messages arrive one or more times, not exactly once. Consumers must be idempotent:
- Store processed event IDs in a dedup table
- Use natural keys (
orderId + eventType) with upserts - Design side effects (charges, shipments) with idempotency keys
Exactly-once end-to-end is expensive. Idempotent consumers are the pragmatic default.
Ordering is partial
Global ordering across an entire system is rare. Kafka offers ordering per partition key. SQS standard queues do not guarantee order. Design consumers so out-of-order events do not corrupt state—or use sequencing metadata and buffers when order matters within an aggregate.
Use the outbox pattern for consistency
Never emit an event before the database transaction commits. The transactional outbox writes events to an outbox table in the same DB transaction, then a relay publishes to the broker. This prevents "event fired but DB rolled back" nightmares.
Version your schemas
Add fields, don't rename in place. Consumers ignore unknown fields. Producers populate defaults. Breaking changes get new event types or version suffixes (OrderPlacedV2).
Monitor consumer lag
User-visible bugs often show up as growing queue depth while APIs look healthy. Alert on:
- Oldest unprocessed message age
- DLQ insert rate
- Consumer error rate per handler
Saga patterns for distributed workflows
Long business processes—checkout, onboarding, loan approval—span multiple services. Two coordination styles dominate:
Choreography — Each service listens and emits events. No central coordinator. Simple early on; hard to visualize when ten services participate.
Orchestration — A saga manager issues commands and tracks state. Easier to audit; risk of becoming a god service if boundaries blur.
Start choreographed for two or three steps. Introduce orchestration when flows branch heavily or compensations multiply.
Compensating transactions (refunds, inventory release) should be explicit events, not hidden rollbacks.
EDA on AWS (a concrete sketch)
Many teams on AWS combine:
- SNS for fan-out fan-in pub/sub
- SQS for durable per-consumer queues with backpressure
- EventBridge for schema discovery and routing rules across accounts
- Kinesis when replay and high throughput matter
Example: OrderPlaced hits EventBridge → rules route to SQS queues for email, fraud scoring, and data lake ingestion. Each consumer scales on its own Auto Scaling group or Lambda concurrency.
Pick one primary bus per bounded context. Running Kafka, RabbitMQ, and EventBridge for the same product without boundaries creates operator fatigue.
Testing event-driven systems
Contract tests — Producers publish sample events; consumers assert they handle required fields.
In-memory brokers — Testcontainers with Kafka or LocalStack for integration tests.
Record/replay — Capture production traffic (sanitized) to reproduce consumer bugs.
Unit tests alone miss serialization mismatches and retry behavior. Invest in broker-backed integration tests for critical paths.
Migration path from monolith
- Identify boundaries where async is already happening (cron jobs, email side effects).
- Extract those side effects behind an outbox + queue without splitting the monolith yet.
- Move consumers into separate deployables one at a time.
- Introduce schema registry once more than two producers share a topic.
Big-bang "everything is events" rewrites fail. Incremental extraction wins.
FAQ
Is EDA the same as microservices?
No. You can run events inside a monolith (in-process bus) or use microservices synchronously. They often appear together but are independent choices.
Kafka vs SQS?
Kafka excels at high-throughput logs, replay, and stream processing. SQS excels at simple task queues with managed ops and per-message DLQs. Many systems use both for different jobs.
How do I debug a missed event?
Trace correlation IDs from HTTP request through outbox row to broker message ID to consumer logs. Without that chain, you are guessing.
What about CQRS?
Command Query Responsibility Segregation often pairs with EDA: writes emit events; read models update asynchronously. Powerful for read scaling; adds consistency lag to account for in UX.
Can events replace all APIs?
No. Users still need synchronous reads and imperative actions. EDA complements APIs; it rarely replaces them entirely.
Event-driven architecture is not free complexity—it is borrowed flexibility. Used deliberately, it lets systems grow without tangling every service in every other service's release cycle. Used casually, it turns a simple feature into a distributed mystery. Start with clear domain events, idempotent consumers, and observability before you celebrate the decoupling.
Comments
Loading comments…