Picking the wrong default between events and requests is a decision you pay for in every feature after. Not dramatically, not all at once — but in the grinding accumulation of coupling, the tight-deadline workarounds, the "we just need to call service B directly for now" that calcifies into permanent architecture.
I've reviewed dozens of architectures across reinsurance, energy, insurance, and SaaS companies over the past decade. The teams that chose badly between these two patterns rarely made an obviously wrong decision. They made a locally reasonable one, optimised for the problem in front of them, and inherited a structural liability they didn't see coming for another eighteen months.
The Definitions That Actually Matter
Request-driven architecture means one service explicitly calls another and waits for a response. The caller knows the callee. HTTP and gRPC are the canonical patterns. The flow is synchronous by default, and the caller and callee are coupled — at minimum by interface contract, often by availability and latency too.
Event-driven architecture means a service emits an event describing something that happened. It does not know, or care, who processes that event. A broker (Kafka, Kinesis, RabbitMQ, Azure Service Bus) sits in between. Consumers subscribe and process asynchronously. The producer and consumers are decoupled — in deployment, in scaling, often in team ownership.
Neither is universally superior. The failure mode I see most often is not choosing the wrong pattern outright; it's applying the wrong default everywhere because the team learned one tool well and reached for it regardless of fit.
When Request-Driven Is Correct
Direct service calls are the right choice when a synchronous response is genuinely part of the user contract. If a customer clicks "check eligibility" and needs a yes/no answer in under two seconds, making that asynchronous is engineering indulgence, not engineering discipline. You'd be adding a polling loop or webhook infrastructure to solve a problem that a direct call handles cleanly.
Request-driven is also correct when you need strong consistency. If two operations must succeed or fail together — a payment and an inventory deduction — you want to control that transaction boundary explicitly. Event-driven patterns can achieve consistency, but they do so through saga choreography or orchestration, which adds significant complexity. For simple, bounded transactional needs, that's overkill.
Finally, direct calls suit low-volume internal tooling where operational simplicity outweighs architectural elegance. A nightly reporting job that calls three internal services in sequence is not a messaging candidate. The blast radius of coupling is low and the operational overhead of a broker is not worth it.
The synchronous response test
Before adding a message broker, ask: does the caller genuinely need a response to proceed, and does the user experience require that response within the request cycle? If yes to both, request-driven is almost certainly correct. If you find yourself wanting to "just make it async to decouple it" but the response is still required synchronously, you'll end up with the worst of both worlds — async infrastructure with a synchronous constraint bolted on top.
When Event-Driven Is Correct
Event-driven architecture earns its complexity premium in four specific situations.
When multiple consumers need to react to the same state change. A policy is underwritten. Now: billing needs to generate an invoice, the customer portal needs to update its state, the fraud model needs to score the new policy, and the regulatory reporting system needs to log it. If you model this as direct calls, you have four outbound HTTP calls from the underwriting service — and a new coupling every time a new consumer appears. You also have a blast-radius problem: if the fraud model service is down, does the underwriting transaction fail? With an event, each consumer is independent. The underwriting service emits PolicyUnderwritten and moves on.
When you need to decouple teams, not just services. This is underappreciated. In a large engineering organisation, the value of events is not primarily technical — it's organisational. The team that owns the order management domain should not need to coordinate a deployment with the team that owns the notification domain. An event published to a stable topic decouples their release cycles entirely. When I'm helping an organisation beyond about 40 engineers, this is often the dominant argument for moving key flows to events.
When processing needs to survive spikes or failures independently. Energy trading platforms. Insurance claims ingestion. E-commerce during peak hours. The pattern is the same: a bursty upstream source should not be able to bring down a downstream processor. A queue or stream between them gives the consumer a buffer. It can process at its own rate, apply back-pressure, and recover from failure without losing events. You cannot achieve this safely with synchronous calls without implementing retry, circuit breaker, and timeout logic that collectively approximates what a broker gives you for free.
When you need a reliable audit trail by default. Events in Kafka or similar systems are durable, ordered, and replayable. You can reprocess historical events to rebuild a read model, debug a production anomaly, or onboard a new downstream consumer against existing data. This is not a feature you bolt on — it's a structural property of the architecture. Request-driven systems achieve this only with explicit additional instrumentation.
The Decision Heuristic I Actually Use
After enough architecture reviews, I've boiled this down to a sequence of questions.
| Question | Answer | Default |
|---|---|---|
| Does the caller need a real-time response to continue? | Yes | Request-driven |
| Does more than one consumer need to react to this event? | Yes | Event-driven |
| Do the producer and consumer teams deploy independently? | Yes | Event-driven |
| Is the flow bursty or do you need failure isolation? | Yes | Event-driven |
| Is this low-volume internal tooling with one consumer? | Yes | Request-driven |
| Does this cross a domain boundary with complex business logic? | Yes | Event-driven |
| Is strong transactional consistency required? | Yes | Request-driven (or saga if scale demands it) |
Work through the questions in order. The first yes wins. If you reach the end without a clear signal, you almost certainly have a request-driven case — events add operational overhead that should only be accepted for an identified benefit.
The decision logic flows like this:
The domain boundary rule
Events are architecture's natural domain boundary markers. When I see a UserService making a direct call into BillingService to trigger invoice generation, I'm looking at a domain boundary violation wearing the clothes of a technical decision. The question isn't "should this be async?" — it's "should UserService even know that invoice generation happens?" Almost always, the answer is no. An event published by UserService and consumed by BillingService makes that boundary explicit and enforced by structure, not by convention.
Where Teams Get This Wrong
Misapplying events to simple CRUD
The most common mistake I see in startup architectures: everything becomes a Kafka topic. Profile update? Kafka. Password reset? Kafka. Admin flag toggle? Kafka. The team has learned that events are good and applied them uniformly. The result is a broker dependency for operations that don't need durability, fan-out, or decoupling — and a debugging experience that turns a simple "why didn't the password reset email send?" into a distributed trace investigation.
Events should carry business significance. PolicyUnderwritten, OrderShipped, PaymentFailed — these are domain events with clear semantic meaning and multiple plausible consumers. UserUpdatedTheirDisplayName is a CRUD operation. The distinction matters.
Building request-driven systems and calling them event-driven
This is subtler and more damaging. The team adds Kafka but then builds a consumer-per-producer relationship where each event has exactly one consumer, that consumer's identity is known to the producer, and the producer blocks or retries based on consumer acknowledgement. Congratulations — you have HTTP over Kafka. You've added the operational complexity of a message broker without gaining the decoupling benefit.
True event-driven design means the producer is genuinely unaware of consumers. If a new consumer appears, the producer doesn't change. If that constraint makes you uncomfortable — if there are things the producer "needs" to know about consumer processing — you probably have a request-driven workflow that would be cleaner as a direct call.
Underestimating the operational overhead of events
Kafka is excellent infrastructure. It is not simple infrastructure. You need to think about partitioning strategy, consumer group offsets, dead-letter queue handling, schema evolution, at-least-once vs. exactly-once delivery semantics, and message ordering guarantees. Teams that add Kafka to a 15-person startup to get "scalability" are often trading six months of feature velocity for infrastructure they won't need for two years.
The managed options reduce but don't eliminate this overhead. Azure Event Hubs, AWS Kinesis, Confluent Cloud — they remove the cluster management burden, not the design burden.
Schema evolution is the long-term cost nobody plans for
Event schemas are public contracts. Once a consumer depends on the shape of PolicyUnderwritten, changing that shape requires either backward-compatible evolution or coordinated migration across every consumer. In a synchronous API, versioning is explicit and usually in the URL. In an event-driven system, schema changes in a poorly designed setup silently break consumers. Invest in a schema registry from day one — Confluent Schema Registry, AWS Glue, Azure Schema Registry. This is not optional infrastructure; it's what separates event-driven systems that last from ones that collapse under their own evolution.
The Hybrid Architecture That Actually Works
In practice, mature systems use both patterns deliberately. Here's what I typically see in a well-designed architecture at 100-500 engineers:
- Synchronous request-driven for: user-facing queries, read operations, admin APIs, and any flow where the response is part of the immediate user experience
- Event-driven for: cross-domain state changes, workflows that fan out to multiple consumers, integration with external partners, audit trails, and anything that benefits from temporal decoupling
The boundary between them should be drawn along domain lines, not technology preference. When I help a team refactor a tightly coupled codebase, I'm not asking "what can we make async?" I'm asking "where are the domain boundaries, and are they structurally enforced or just agreed by convention?" Events enforce domain boundaries. Direct calls rely on discipline. At scale, discipline is insufficient.
A well-structured hybrid system draws the boundary clearly:
Making the Decision With Confidence
There is no universal right answer here, but there is a wrong process: choosing based on familiarity, choosing based on what looked impressive in the architecture diagram, or not choosing at all and letting coupling accumulate by default.
When I sit with an engineering leadership team working through this decision, the conversation that unlocks it most reliably is not "what technology should we use?" but "what needs to happen when X occurs, and who needs to know?" Map the state change, map the consumers, and map the team ownership. The architecture choice follows naturally from that mapping. If the mapping shows one consumer that the caller already knows about and a response that's needed immediately, request-driven. If the mapping shows multiple consumers, independent teams, or a need for durability — events.
The choice you make becomes your default. Your default shapes every feature after it. Get the default right.
If you're designing a new system or untangling an architecture that's developed the wrong default over time, let's talk — a 30-minute discovery call is usually enough to map the decision clearly and give you a concrete path forward.