Event-Driven#
The natural fit for an operator’s collection-and-analysis pipeline. recon hits, sensor alerts, and capture events get published to a broker; enrichment, scoring, indexing, and audit consume them independently. Components communicate by publishing and subscribing to events on a broker instead of calling each other directly. The producer doesn’t know who consumes; consumers don’t block the producer; new analytics can be added mid-campaign without touching upstream collection.
flowchart LR
subgraph P[Producers]
order[Order]
payment[Payment]
shipping[Shipping]
end
broker[(Event Broker)]
subgraph C[Consumers]
email[Email]
analytics[Analytics]
search[Search Indexer]
audit[Audit Log]
end
subgraph S[Stores]
emailDb[(Email Provider)]
warehouse[(Warehouse)]
searchDb[(Search Index)]
logs[(Append-Only Log)]
end
order --> broker
payment --> broker
shipping --> broker
broker --> email
broker --> analytics
broker --> search
broker --> audit
email --> emailDb
analytics --> warehouse
search --> searchDb
audit --> logs
Structure#
The three roles in any event-driven system. The producer doesn’t know who consumes; consumers can come and go without the producer noticing. The broker is what decouples them, and becomes the system’s most important critical dependency.
Producer publishes events describing what happened (past tense).
Broker stores or routes events.
Consumer subscribes to events relevant to it.
No request/response between producer and consumer; the producer doesn’t wait.
Event vs. Command#
The distinction that separates event-driven from
request/response. Modeling things as events instead of commands
is the central shift; OrderShipped (event) lets many
consumers react; ShipOrder (command) implies a single
target.
Command, “do X”. Imperative. Caller expects an action and usually a result. Synchronous makes sense.
Event, “X happened”. Past tense. Many subscribers may react. Async by nature.
Modeling things as events instead of commands is the central shift.
OrderShipped is an event; ShipOrder is a command.
Brokers#
The four broker types serve different consumer models. Pick by replay needs, ordering guarantees, retention, and how many independent consumers will subscribe; the choice rarely changes after launch.
Queues (SQS, RabbitMQ), one consumer per message; load leveling between producers and consumers. Messages disappear once consumed.
Logs / streams (Kafka, Kinesis, Pulsar), replayable; many independent consumers each track their own position. Messages persist.
Pub/sub (NATS, MQTT, Cloud Pub/Sub), broadcast to subscribers; no replay.
Event buses (EventBridge, Azure Event Grid), routing layer with filters and rules.
Pick by replay needs, ordering guarantees, retention, and consumer model.
Strengths#
What event-driven buys that synchronous request/response can’t match. Loose coupling and fan-out are the structural wins; buffering and replay are the operational wins; the organizational scaling is the under-counted reason teams adopt the pattern.
Loose coupling, producers and consumers evolve independently.
Broadcast / fan-out, new consumers join without changing producers.
Buffering, consumers can be slower than producers without blocking.
Replay, log-based brokers let you rebuild state or backfill new consumers.
Async-first scales further organizationally; teams don’t need to call each other to build features.
Costs#
The bill that comes with the loose coupling. Eventual consistency, broker dependency, awkward request/response, mandatory idempotency, harder debugging, and ongoing schema evolution all become standing concerns rather than implementation details.
Eventual consistency by default, the world catches up to the event over time.
Broker is a critical dependency, if it goes down, everything pauses.
Harder request/response semantics, if a consumer needs to send a result back, you’re inventing correlation IDs and reply queues.
Idempotency required, “exactly once” is a marketing term; design consumers to handle duplicates.
Debugging is harder, the call stack is gone; you trace via event IDs.
Schema evolution matters, producers and consumers drift; use a schema registry.
Patterns#
The four patterns that come up most often in production event-driven systems. Event sourcing for audit-perfect history, CQRS for write/read divergence, sagas for multi-service workflows, outbox for atomic write+publish.
Event sourcing
Persist the sequence of events that changed state, not just the current state. Current state is derived by folding events.
Strong audit trail by construction.
Easy to add new projections after the fact.
Hard parts: schema evolution of events, snapshotting for performance, cross-aggregate consistency.
CQRS (Command Query Responsibility Segregation)
Writes through validated command handlers against a normalized model.
Reads come from one or more denormalized projections.
The two sides communicate via events.
Often paired with event sourcing.
Saga
A long-running business process that spans multiple services.
Choreography, each service reacts to events and emits new ones; no central coordinator. Simple at small scale; hard to follow at large.
Orchestration, a coordinator drives the steps and handles failures. Easier to reason about, but the coordinator is a critical service.
Compensation is rarely a perfect inverse; build for eventual consistency.
Outbox
Write business state and an “outbox” event in the same DB transaction.
A separate process publishes events from the outbox to the broker.
Solves “at least once” delivery without distributed transactions.
Schemas#
Producer/consumer schemas drift independently in any long-running system. Manage that with a schema registry, explicit compatibility rules, contract tests in CI, and tolerant readers that skip unknown fields rather than failing on them.
Schema registry for events (Avro, Protobuf, JSON Schema).
Compatibility rules, backward, forward, full.
Consumer-driven contract tests to catch drift in CI.
Tolerant readers, ignore unknown fields, don’t fail on additions.
Idempotency#
Build handlers that are safe to call twice. “Exactly once” is not deliverable; “at least once” plus idempotent consumers is the real engineering target. Three approaches.
Each event has a stable ID; consumers dedupe on it.
Update operations are state transitions, not deltas.
When deletion + recreation is possible, store a tombstone.
Ordering#
What guarantees the broker actually offers vs what the application assumes. Per-partition ordering is the standard; global ordering is rare and expensive; partition-key choice is the lever that makes ordering work for your specific correlations (per-tenant, per-order, etc.).
Per-partition ordering is the typical guarantee (Kafka).
Global ordering is rare and expensive.
Pick partition keys carefully; usually a tenant or aggregate ID.
When to Pick Event-Driven#
The fits where event-driven pays off. Most are about fan-out (many consumers per event) or async tolerance (no synchronous answer needed) or audit needs (the log itself is a feature).
Many consumers want to react to the same fact.
Producer doesn’t care who’s listening (“user signed up” → analytics, CRM, welcome email, fraud check).
Flow doesn’t need a synchronous answer.
Throughput is high enough that buffering matters.
Audit trail is required, log-based event storage gives it for free.
When to Skip#
The cases where event-driven is the wrong fit. Synchronous business flows, low-volume CRUD, missing operational capacity, and required strong consistency all push toward request/response.
The business flow really does need a synchronous answer right now (payment authorization).
Low-volume CRUD; the operational complexity of a broker doesn’t pay off.
No engineering capacity to operate the broker.
Strong consistency is required across the affected components.
Common Pitfalls#
The five ways event-driven systems fail in production. Each one is a violation of the basic event-driven invariants; producer doesn’t know consumers, schemas evolve gracefully, ordering assumptions match the broker, sagas are coordinated.
Treating events as commands by another name (one consumer, one producer, one type per event).
Skipping schema evolution; breaking consumers on every change.
No replay or retry strategy; messages get lost on transient failures.
Tight ordering assumptions where the broker doesn’t guarantee them.
Building the saga in 50 places instead of one orchestrator.