The problem
In a request/response world, the service where something happens has to call every service that cares. Placing an order means calling Inventory, then Payments, then Notifications, and so on. That caller must know every downstream service, wait for all of them, fail when any of them fails, and change every time a new one is added. Its latency becomes the sum of theirs, and its availability the product of theirs.
How it works
Services communicate through events: immutable facts about something that happened (OrderPlaced, PaymentCaptured).
- Producers publish events to a broker and move on. They don't know who is listening.
- The broker (Kafka, Amazon SNS + SQS or EventBridge, Azure Service Bus or Event Grid, Google Pub/Sub, RabbitMQ) stores the events and delivers them to every subscription.
- Consumers process events at their own pace. Each consumer can scale, fail and recover independently. Its subscription remembers where it left off.
Adding a new capability usually means adding a new subscriber, with no change to the producer.
When to use it
- Several independent reactions to the same business fact.
- Workloads with bursty traffic, where a queue should absorb spikes instead of the slowest dependency.
- Integrating systems owned by different teams, where temporal coupling (everyone up at the same time) is a liability.
- Streaming and near-real-time analytics that consume the same event log.
Trade-offs
- Eventual consistency. When
OrderPlacedis published, the stock is not reserved yet. UIs and APIs must be designed for that. - Harder to follow. There is no single call stack. You need correlation IDs, distributed tracing and good event catalogs to see a whole flow.
- Delivery semantics. Most brokers are at-least-once: consumers must be idempotent and handle out-of-order events.
- Schema evolution. Events are a public contract. Version them, and prefer additive changes.
- Dual writes. Updating your database and publishing an event is not atomic. Use a transactional outbox or change data capture.
Implementation notes
- Name events in the past tense after business facts (
OrderPlaced), not as commands (SendEmail). - Decide between thin events (IDs only, consumers call back for details) and fat events (carry the data). Fat events reduce coupling at runtime but make the schema a bigger contract.
- Watch consumer lag per subscription. It is the single best health signal in an event-driven system, and a natural autoscaling metric (for example KEDA scaling on queue length).
- Put a dead-letter queue behind every subscription so one poison message can't block the rest.