Event-Driven vs Request-Driven: A Practical Decision Framework
The Question That Starts Every Architecture Review
"Should this be synchronous or asynchronous?" is asked in every microservices architecture review I have been part of. The answers I hear split roughly into two camps: the event-driven evangelists who want to put a message queue in front of everything, and the pragmatists who make a REST call because it is simple and it works.
Both camps are right sometimes. Both camps are catastrophically wrong when they apply their preference without thinking about the problem.
This post is a decision framework. Not a manifesto for events. Not a defence of REST. A set of questions you should answer before choosing a communication style — and a clear map from those answers to the right pattern.
The Mental Models
Before comparing, you need a sharp mental model of what each pattern actually is.
Request-Driven (Synchronous)
The caller sends a request and blocks waiting for a response. The caller knows who it is calling. The callee must be available. The response is part of the interaction.
The key property: the result of step 1 is needed to execute step 2. Checkout cannot proceed without knowing whether inventory was reserved. This is inherently synchronous.
Event-Driven (Asynchronous)
The producer emits an event describing something that happened. It does not know who, if anyone, is listening. It does not wait for a response. Consumers receive the event when they are ready and process it independently.
The key property: the producer does not care what happens next. Three consumers or thirty — the producer code does not change. This is the fan-out superpower of events.
The Decision Framework
Run through these questions in order. The first question that has a clear answer typically determines your pattern.
Let me walk through each question with the reasoning behind it.
Q1: Does the caller need the result to proceed?
This is the most important question and the most misunderstood. "Need" is the operative word. Teams often say they need the result when they actually mean they want confirmation.
- Order service charging a card: needs the result. Cannot confirm the order without knowing if payment succeeded. → Request-Driven.
- Order service sending a confirmation email: does not need the result. Email delivery failure does not cancel the order. → Event-Driven.
If you find yourself writing await on a message queue consumer to get the response back, you have built synchronous request-response over an async channel. You have the worst of both worlds: the latency of async with the coupling of sync. Use REST instead.
Q2: Does more than one consumer need this?
Events scale horizontally in a way that synchronous calls cannot. When an order is placed, the following might all care:
- Notification service (confirmation email + SMS)
- Analytics service (funnel tracking)
- Warehouse service (pick list generation)
- Loyalty service (points calculation)
- Fraud service (pattern analysis)
With request-driven, the caller must know about all of them and call each one. When you add the sixth consumer, you modify the caller. With events, you add the sixth consumer's subscription and the producer never changes. → Event-Driven wins decisively here.
Q3: Can the consumer be temporarily unavailable?
Events provide buffering. If the notification service is down for ten minutes, Kafka holds the events. When the service comes back, it processes the backlog. No events lost.
Synchronous calls fail immediately when the target is unavailable. You need retries, circuit breakers, and fallback logic in the caller. For fire-and-forget operations where durability matters more than immediacy, events handle this better.
The Patterns in Practice
Pattern 1: Synchronous core, async side-effects
This is the most pragmatic hybrid and the one I use most often. The primary transaction path is synchronous. Side-effects (notifications, analytics, cache invalidation) are events.
The transactional outbox pattern is the glue here. You write the order and the outbox event in the same database transaction. A background publisher reads the outbox and publishes to Kafka. This guarantees that if the order is persisted, the event will eventually be published — even if the service crashes immediately after the write.
// Transactional outbox — write order + event atomically
@Transactional
public OrderConfirmation confirmOrder(OrderRequest request) {
Order order = orderRepository.save(new Order(request));
OutboxEvent event = OutboxEvent.builder()
.aggregateType("Order")
.aggregateId(order.getId().toString())
.eventType("OrderConfirmed")
.payload(objectMapper.writeValueAsString(new OrderConfirmedEvent(order)))
.build();
outboxRepository.save(event); // Same transaction as order
return new OrderConfirmation(order.getId(), order.getStatus());
}Pattern 2: Event-driven with request-driven fallback
When the event consumer needs data the event does not carry, it can make a synchronous call to fetch it. The event triggers the action; the request fetches the context.
// Consumer: handle OrderConfirmed event, fetch details synchronously
@KafkaListener(topics = "order.confirmed")
public void handleOrderConfirmed(OrderConfirmedEvent event) {
// Event carries orderId — fetch full order details synchronously
Order order = orderClient.getOrder(event.getOrderId());
notificationService.sendConfirmationEmail(
order.getCustomerEmail(),
order.getItems(),
order.getShippingAddress()
);
}This is a valid and common pattern. Events for triggering, REST for enrichment.
Common Mistakes
Mistake 1: Async request-response over Kafka
Service A → publishes "GetUserRequest" to Kafka
Service B → consumes, processes, publishes "GetUserResponse" to reply topic
Service A → blocks waiting for replyThis is synchronous communication cosplaying as async. You get the operational complexity of Kafka (ordering guarantees, consumer group management, offset tracking) with the latency characteristics of blocking calls. Just use gRPC.
Mistake 2: Events with embedded commands
Events should describe what happened, not instruct what to do.
// Bad — command disguised as an event
{
"type": "OrderPlaced",
"action": "SEND_EMAIL",
"template": "order_confirmation",
"recipient": "user@example.com"
}
// Good — pure event, consumer decides what to do
{
"type": "OrderPlaced",
"orderId": "ord-123",
"customerId": "cust-456",
"placedAt": "2026-04-11T14:30:00Z",
"totalAmount": 89.99
}When the event contains a command (action: SEND_EMAIL), the producer now has knowledge of what consumers do. That coupling defeats the decoupling purpose of events.
Mistake 3: Ignoring ordering guarantees
Kafka provides ordering within a partition. If you need strict ordering across all events for an entity, you must ensure all events for that entity go to the same partition. Use the entity ID as the partition key.
// Producer — partition by orderId to guarantee ordering
ProducerRecord<String, OrderEvent> record = new ProducerRecord<>(
"order.events",
order.getId().toString(), // Partition key = orderId
new OrderStatusChangedEvent(order)
);When to Mix Both in One Flow
A well-designed system often uses both patterns in a single business flow. Here is a complete order processing flow that illustrates appropriate use of each:
The critical path (client → API → payment → response) is synchronous because the client needs confirmation. Everything after the confirmation is asynchronous because the client is already gone.
The Right Question Is Not "Which Is Better?"
It is "what does this specific interaction require?"
- Need the result: use synchronous.
- Fan-out to multiple consumers: use events.
- Temporal decoupling required: use events.
- Simple point-to-point, result needed: use synchronous.
- Audit trail, replay, backpressure: use events.
Most real systems use both patterns, applied judiciously. The systems that get into trouble are the ones that chose a pattern as a religion rather than as a tool. Events are not more "modern" than REST. REST is not more "simple" than Kafka. They solve different problems. Respect the difference.