On this page
What is event-driven architecture?
How does request-response handle an order?
How does the event-driven version handle the same order?
What are the main event-driven architecture patterns?
Event notification
Event-carried state transfer
Event sourcing
CQRS
How do sagas coordinate work across services?
How does the outbox pattern stop lost events?
What are the hard parts of event-driven systems?
Ordering
At-least-once delivery and idempotent consumers
Schema evolution
Eventual consistency
Debugging and tracing
When should you use events over request-response?
Event-driven vs request-response compared
How to explain event-driven architecture in a system design interview
Frequently asked questions
Related reading
Event-Driven Architecture: Patterns, Diagrams, and When to Use It Over Request-Response


On This Page
What is event-driven architecture?
How does request-response handle an order?
How does the event-driven version handle the same order?
What are the main event-driven architecture patterns?
Event notification
Event-carried state transfer
Event sourcing
CQRS
How do sagas coordinate work across services?
How does the outbox pattern stop lost events?
What are the hard parts of event-driven systems?
Ordering
At-least-once delivery and idempotent consumers
Schema evolution
Eventual consistency
Debugging and tracing
When should you use events over request-response?
Event-driven vs request-response compared
How to explain event-driven architecture in a system design interview
Frequently asked questions
Related reading
Event-driven architecture is a way of building a system where services announce what has happened, and other services react to those announcements on their own schedule. A service that takes an order does not call the payment, inventory, and email services one by one. It records the order, publishes a short message that says "an order was placed", and moves on. Every service that cares about new orders receives that message and does its own work.
Use it over request-response when several services must react to the same fact, when the caller does not need their results to finish its own job, or when one slow service must not slow down the others. Keep request-response when the caller needs an answer right now, such as a price check or a login. Most real systems use both styles, and the design skill is choosing which style each interaction gets.
This article builds one e-commerce order flow twice, first with direct calls and then with events. It then explains the core patterns, the parts that are hard in production, and a decision guide with a comparison table.
What is event-driven architecture?
An event is a record of something that has already happened, stated in the past tense. "OrderPlaced", "PaymentCaptured", and "ItemShipped" are events. An event does not ask anyone to do anything. It states a fact, and it cannot be refused, because the thing it describes is already true.
A command is different. A command is a request for something to happen, such as "CapturePayment" or "ReserveStock". It has one intended receiver, and that receiver can say no. This difference matters more than any technology choice. When a team sends commands through a message broker and calls the result event-driven, it usually keeps all the coupling of direct calls and adds the cost of a broker on top.
Three roles appear in every event-driven system:
- The service that publishes an event (the producer). In our example, the order service publishes OrderPlaced after it saves the order.
- The system that stores and delivers events (the broker). Kafka, RabbitMQ, Amazon SNS with SQS, and Google Pub/Sub are common brokers. The broker keeps each event until the consumers that need it have received it.
- A service that receives events and acts on them (the consumer). The payment, inventory, email, and analytics services are all consumers of OrderPlaced.
The producer does not know who the consumers are. It knows only the event and the topic it publishes to. That single fact explains both the main benefit of the style and most of its difficulty, which the rest of this article covers.
How does request-response handle an order?
In request-response, one service calls another and waits for the reply before it continues. Our running example is a shop where a customer places an order. With direct calls, the order service does the following:
- Saves the order in its own database.
- Calls the payment service to charge the card and waits for the result.
- Calls the inventory service to reserve the items and waits.
- Calls the email service to send a confirmation and waits.
- Calls the analytics service to record the sale and waits.
- Returns "order confirmed" to the customer.
Each step is easy to read, and the flow is easy to debug, because one request produces one call chain. The problems appear when the numbers grow.
Take some illustrative timings: 120 ms for payment, 40 ms for inventory, 300 ms for email, and 30 ms for analytics. The customer waits for all of them added together, about 490 ms plus the order service's own work. The email service is the slowest step, yet the customer does not need the email to finish before the page says "confirmed".
Availability multiplies in the same way. If each of the four services is up 99.9 percent of the time, the chance that all four are up together is 0.999 to the fourth power, about 99.6 percent. That gap is roughly 35 hours a year when an order fails because some service that the customer never sees is down. A failed analytics call should not cancel a sale, but in a strict call chain it can.
The last problem is change. When the marketing team wants a loyalty service to award points for each order, someone must edit the order service to add a fifth call. The order service ends up knowing about every team that cares about orders, which is the definition of tight coupling (one part must change whenever another part changes).
How does the event-driven version handle the same order?
The event-driven version splits the flow into the part the customer must wait for and the part that can happen afterward. Payment usually stays in the first part, because the shop should not confirm an order it cannot charge. Everything else becomes a reaction to an event.
- The order service saves the order and calls payment directly, as before.
- When payment succeeds, the order service publishes an OrderPlaced event to the broker.
- It returns "order confirmed" to the customer.
- The inventory, email, analytics, and loyalty services each receive OrderPlaced and do their own work, in parallel and at their own pace.

With the same illustrative numbers, the customer now waits for the order write, the payment call, and the publish, about 160 ms instead of about 490 ms. If the email service is down for ten minutes, orders still succeed. The OrderPlaced events wait in the broker, and the email service sends the confirmations when it recovers.
Adding the loyalty service no longer requires a change to the order service. The loyalty team subscribes to OrderPlaced and deploys on its own schedule. This property is called loose coupling: the producer and consumers agree on the event format and nothing else.
The cost is that the system no longer has one place where the whole flow is visible. The customer sees "confirmed" before the stock is reserved, so the design must decide what happens when the last item was sold a second earlier. The next sections cover the patterns that answer questions like that one.
What are the main event-driven architecture patterns?
Martin Fowler's article "What do you mean by Event-Driven?" groups the ideas into four patterns. They are often mixed up, and they solve different problems, so it helps to name each one precisely.
Event notification
The event says only that something happened and carries an identifier, such as {"type": "OrderPlaced", "orderId": "o-81"}. A consumer that needs more detail calls the producer to fetch it. This pattern is small and simple, and it keeps the event format stable.
Its weakness is that every consumer calls back to the order service. Four consumers mean four extra reads per order, and the order service must stay available for consumers to do their work. That brings back some of the coupling that events were meant to remove.
Event-carried state transfer
The event carries all the data consumers need: the items, the quantities, the customer's email address, and the total. The email service can send the confirmation without calling anyone. Consumers often keep a local copy of the data they receive, so they can answer their own queries during an outage of the producer.
The trade is size and duplication. Events become larger, the same data lives in several services, and every copy is slightly behind the source. For most order flows this is an acceptable trade, and it is the pattern most production systems end up using.
Event sourcing
In event sourcing, the events are the record itself. Instead of storing an order row with a status column, the service stores the sequence of events that happened to the order: OrderPlaced, PaymentCaptured, ItemShipped, ItemReturned. The current state is calculated by replaying those events in order.
This pattern gives a complete history and makes questions like "what did this order look like on Tuesday?" easy to answer. It also makes schema changes and corrections harder, because past events cannot be edited. Event sourcing is a storage decision for one service. A system can be event-driven without using event sourcing anywhere.
CQRS
Command Query Responsibility Segregation (CQRS) means one model handles writes and a separate model handles reads. The write side accepts commands and produces events. The read side consumes those events and builds views shaped for each query, such as "orders by customer" or "sales per hour".
CQRS helps when reads and writes have very different shapes or volumes, for example a store that takes 200 orders a second and serves 20,000 order-history page views a second. The cost is that the read views lag the write side by a short delay, and there are now two models to keep correct.
Want to practice these patterns on real designs? Grokking Kafka and Event-Driven Architecture walks through each pattern with the design decisions and trade-offs an interviewer expects you to explain.
How do sagas coordinate work across services?
A business process that spans several services cannot use one database transaction, because each service owns its own database. A saga is a sequence of local transactions, one per service, where each step either succeeds or triggers compensating steps that undo the earlier ones. If stock reservation fails after payment succeeded, the compensating step refunds the payment.
There are two ways to run a saga, and they differ in who decides what happens next.
Choreography means no central coordinator. Each service listens for events and decides its own next action. The inventory service hears PaymentCaptured and reserves stock, then publishes StockReserved. The shipping service hears StockReserved and creates a shipment. Choreography keeps services independent, and it works well for flows with three or four steps.
Orchestration means one service, the orchestrator, tells each participant what to do by sending commands and waiting for replies. An order orchestrator sends CapturePayment, then ReserveStock, then CreateShipment, and it runs the compensations when a step fails. The whole flow lives in one place, which makes it easier to read, monitor, and change.

A practical guide is to count the steps and the branches. With four or more steps, or with conditional paths such as "ship from a second warehouse if the first is out of stock", choreography becomes hard to follow. The flow then exists only as a set of subscriptions spread across services. Orchestration is usually the better choice there. The Saga Pattern vs. Two-Phase Commit post covers why two-phase commit is rarely used between services.
How does the outbox pattern stop lost events?
The order service must do two things when an order is placed: save the order in its database and publish OrderPlaced to the broker. These are two separate systems, so one can succeed while the other fails. If the database write succeeds and the publish fails, the order exists but no other service ever hears about it. This is called the dual-write problem.
The transactional outbox pattern solves it. The service writes the order row and an outbox row describing the event in the same database transaction, so both are saved or neither is. A separate process (the relay) reads new outbox rows and publishes them to the broker, then marks them as sent. If the relay crashes after publishing but before marking the row, it publishes the same event again after it restarts.

That last detail means the outbox delivers each event at least once, not exactly once. Every consumer must therefore be idempotent, which means that processing the same event twice has the same result as processing it once. The usual method is to store each processed event ID and skip any ID already seen. The Transactional Outbox Pattern post covers the relay options in detail, including change data capture, which reads the outbox rows from the database log.
What are the hard parts of event-driven systems?
Events remove the call chain, but they add problems that a synchronous design never has. Each of the following needs a deliberate answer in the design.
Ordering
Brokers guarantee order only within a limited scope. Kafka keeps events in order within one partition, not across the whole topic. RabbitMQ keeps order within one queue, but several consumers on that queue can finish messages out of order. If OrderCancelled is processed before OrderPlaced, a consumer can end up with a cancelled order that it then marks as active.
The standard fix is to route all events for one entity to the same partition by using the entity ID as the key. All events for order o-81 then arrive in order, while events for different orders still spread across partitions. Consumers should also carry a version number per entity and ignore any event older than the version they already hold.
At-least-once delivery and idempotent consumers
Most brokers deliver at least once by default. A consumer that crashes after doing the work but before acknowledging the message receives it again. Without idempotency, the customer gets two confirmation emails or, worse, two loyalty awards.
Exactly-once delivery across a network is possible only inside narrow limits, such as Kafka transactions between Kafka topics. The safe assumption is that duplicates will arrive and that every consumer must handle them. The Exactly-Once Delivery post explains where those limits sit.
Schema evolution
Events are a contract between teams that do not deploy together. If the order team renames total to amount, every consumer that reads total breaks, possibly days later when it next deploys. The rule is to make only backward-compatible changes: add optional fields, never remove or rename fields in place, and publish a new event version when a breaking change is unavoidable.
A schema registry, a service that stores the approved schema for each event type and rejects producers that break compatibility, enforces this rule automatically. Formats such as Avro and Protobuf work well with a registry, because their compatibility rules are explicit.
Eventual consistency
After the customer sees "confirmed", the inventory count, the order-history page, and the analytics dashboard each update a little later. This is eventual consistency: all copies agree after a short delay, but not at every instant. With a healthy broker the delay is often under one second, but during an outage it can grow to minutes.
The design must state what the user sees during that delay. A common answer is to show the order as "processing" until the stock reservation event arrives, and to send an apology with a refund in the rare case that stock runs out. The Eventual vs Strong Consistency post covers how to choose between the two models.
Debugging and tracing
In a call chain, one stack trace shows the whole request. In an event-driven flow, the work is spread across services and time. An order that never got its confirmation email could be stuck in the broker, failing in the email consumer, or skipped because of a schema mismatch.
Two practices make this manageable. First, attach a correlation ID (one identifier that travels with the order through every event and log line) so that a single search finds every step. Second, send messages that fail repeatedly to a dead-letter queue, a separate queue for messages a consumer could not process, so they can be inspected and replayed instead of blocking the queue or disappearing. Distributed tracing tools such as OpenTelemetry can carry the trace context inside event headers, which links the consumer's work back to the original request.
When should you use events over request-response?
Choose events for an interaction when most of the following are true:
- Several services react to the same fact. One OrderPlaced event serves inventory, email, analytics, and loyalty without the producer knowing any of them.
- The caller does not need the result to continue. The customer does not wait for the email to be sent.
- Slow or failing consumers must not affect the producer. The email service can be down for an hour without stopping sales.
- Load arrives in spikes. The broker absorbs a burst of 10,000 orders a minute, and consumers work through it at their own rate.
- You need a history to replay. A new analytics model can rebuild its data by reading old events from a broker that retains them, such as Kafka.
Keep request-response when the following are true:
- The caller needs the answer now. Checking a password, quoting a price, or confirming that a seat is free all need an immediate reply.
- The operation must succeed or fail as one unit from the user's view. Payment authorization usually stays synchronous for this reason.
- The flow is simple and owned by one team. Two services that always change together gain little from a broker and pay its full operational cost.
- The team cannot yet operate a broker. A broker is another stateful system to run, monitor, and upgrade, with its own failure modes.
A useful test for each interaction is to ask whether the caller would behave differently based on the reply. If it would, the interaction is a request. If it would not, it is probably an event.
Event-driven vs request-response compared
| Aspect | Request-response | Event-driven |
|---|---|---|
| Who knows whom | Caller knows every callee | Producer knows only the topic |
| Latency seen by the user | Sum of every call | Only the synchronous part |
| Effect of a slow consumer | Slows the whole request | Consumer falls behind; producer is unaffected |
| Consistency | Immediate within the call chain | Eventual across services |
| Adding a new reaction | Change and redeploy the caller | Add a new consumer |
| Debugging | One trace per request | Needs correlation IDs and tracing |
| Delivery guarantees | One call, one reply | Usually at least once; consumers must be idempotent |
| Best fit | Queries, payment authorization, simple flows | Fan-out, spikes, independent teams, audit history |
How to explain event-driven architecture in a system design interview
Name the style of each interaction instead of declaring the whole system event-driven. A strong answer for the shop sounds like this: "Payment stays synchronous, because we cannot confirm an order we cannot charge. Everything after that reacts to an OrderPlaced event, keyed by order ID so events for one order stay in order."
Then cover the three follow-up questions that almost always come next. Say how events reach the broker reliably (the outbox), how consumers handle duplicates (idempotency with stored event IDs), and what the user sees while the system catches up (a "processing" state). An answer that covers those three points shows that you have run such a system and not only drawn one.
Frequently asked questions
What is the difference between an event and a message? A message is any data sent through a broker. An event is one kind of message that records a fact in the past tense, such as OrderPlaced. A command is another kind, which asks one receiver to do something, such as CapturePayment.
Is event-driven architecture the same as microservices? No. Microservices describe how a system is split into separately deployed services. Event-driven architecture describes how those services communicate. Microservices can talk only through direct calls, and a single application can use events internally.
Is Kafka required for event-driven architecture? No. Kafka suits high volume and replay because it keeps events for a configured period. RabbitMQ, Amazon SNS with SQS, and Google Pub/Sub are also common choices. The patterns in this article apply to any of them. The Kafka vs RabbitMQ post compares the two most common brokers.
What is the difference between event-driven architecture and pub/sub? Publish-subscribe (pub/sub) is a delivery method where a publisher sends to a topic and every subscriber receives a copy. Event-driven architecture is the wider design style that uses pub/sub, along with patterns such as sagas, the outbox, and event sourcing.
How do you handle a failed event in a consumer? Retry a small number of times with a growing delay, then move the event to a dead-letter queue. Alert on the dead-letter queue, fix the cause, and replay the events. Because consumers are idempotent, a replay cannot cause duplicate effects.
Should I use choreography or orchestration? Choreography suits short flows of three or four steps with no branching. Orchestration suits longer flows with conditional paths or compensations, because the whole process lives in one readable place. Many systems use orchestration inside a business process and events between processes.
Does event-driven architecture make a system faster? It makes the part the user waits for faster, because slow work moves out of the request. It does not make the total work faster, and each event adds a small delay for publishing and delivery. It is a choice about who waits, not about how much work is done.
Related reading
- Grokking Messaging Patterns: Queues, Pub/Sub, and Event Streams
- The Observer vs Pub-Sub Pattern
- Transactional Outbox Pattern: How to Solve the Dual-Write Problem
- Change Data Capture
- RabbitMQ vs Kafka vs ActiveMQ: Choosing a Message Broker
- Grokking Idempotency in System Design
Ready to design event-driven systems you can defend in an interview? Grokking Kafka and Event-Driven Architecture covers brokers, delivery guarantees, sagas, and the outbox through complete designs you can compare against your own.
What our users say
MO JAFRI
The courses which have "grokking" before them, are exceptionally well put together! These courses magically condense 3 years of CS in short bite-size courses and lectures (I have tried Grokking System Design Interview, OODI, and Coding patterns). The Grokking courses are godsent, to be honest.
ABHISHEK GUPTA
My offer from the top tech company would not have been possible without Grokking System Design. Many thanks!!
Eric
I've completed my first pass of "grokking the System Design Interview" and I can say this was an excellent use of money and time. I've grown as a developer and now know the secrets of how to build these really giant internet systems.
Access to 50+ courses
New content added monthly
Certificate of completion
$31.08
/month
Billed Annually
Recommended Course

Grokking the Object Oriented Design Interview
61,294+ students
4.2
Learn how to prepare for object oriented design interviews and practice common object oriented design interview questions. Master low level design interview.
View Course