Event-Driven vs Request-Driven
Request-driven architectures use synchronous calls where the client waits for a response.
Request-driven architectures use synchronous calls where the client waits for a response. Event-driven architectures use asynchronous events where producers and consumers are decoupled. Each model has distinct implications for latency, coupling, and system complexity.
Which Should You Pick?
Go with Request-Driven if:
- The client needs an immediate response (user-facing APIs)
- Operations are simple and fast (CRUD on a single resource)
- You want straightforward error handling and debugging
- The interaction is inherently request-response (GET user profile)
Go with Event-Driven if:
- Operations are long-running or involve multiple services
- You need to decouple producers from consumers
- The same event triggers actions in multiple downstream systems
- You need load leveling (absorb traffic spikes without overloading backends)
Understanding Request-Driven Architecture
A client sends a request and blocks until it receives a response. HTTP REST APIs, gRPC calls, and traditional database queries follow this pattern.
Strengths: The mental model is simple. Send a request, get a response, handle errors. The call stack is visible in a single trace. Debugging is straightforward — you can reproduce the exact request and observe the exact response. Latency is predictable for fast operations. Error handling is immediate: the client knows right away if something failed.
Weaknesses: Tight coupling between caller and callee. If the downstream service is slow or down, the caller is blocked. Cascading failures spread when one slow service blocks all its callers, which block their callers, and so on. Scaling is constrained because every request holds a connection and a thread (or goroutine) for the duration of the call.
Stripe's payment API is request-driven by design. When you charge a card, you need to know immediately whether it succeeded. The merchant's checkout page blocks until Stripe returns a success or failure. There is no sensible event-driven alternative for this interaction.
Understanding Event-Driven Architecture
Producers emit events to a broker (Kafka, RabbitMQ, SQS). Consumers subscribe to events and process them independently. The producer does not know who consumes the event or when.
Strengths: Full decoupling between producer and consumer. The order service emits an "OrderPlaced" event. The inventory service, email service, analytics service, and fraud detection service each consume it independently. Adding a new consumer requires zero changes to the producer.
Load leveling absorbs traffic spikes. If 10,000 orders arrive in one second, the events queue up in Kafka. The payment processing service consumes them at its own pace (say, 500 per second). No requests are dropped; processing just takes longer.
Fault tolerance improves because events are persisted in the broker. If the email service crashes, the events wait in the queue. When the service recovers, it picks up where it left off. No events are lost.
Weaknesses: Eventual consistency is unavoidable. After emitting an event, the producer does not know when (or if) consumers have processed it. The user places an order and sees "Order Placed," but the inventory has not been decremented yet. If the user immediately checks inventory, it shows the old count.
Debugging is harder. A single business operation might trigger a chain of events across five services. Tracing the full flow requires correlation IDs, distributed tracing, and centralized logging. When something goes wrong, you cannot just look at a stack trace.
Error handling is complex. What happens when a consumer fails to process an event? Dead letter queues capture failed events, but someone must monitor and reprocess them. Idempotency is critical — if an event is processed twice, the outcome must be the same.
Real-World Hybrid Architectures
Most production systems use both patterns. The boundary typically falls along synchronous user-facing operations and asynchronous backend processing.
Uber: The ride request flow is request-driven — the rider taps "Request Ride" and needs immediate confirmation. But once the trip starts, event-driven processing kicks in: trip events flow to the pricing service, the ETA service, the safety monitoring service, and the analytics pipeline. Each consumes the events independently.
LinkedIn: The API gateway handles request-driven traffic for profile views, connection requests, and feed rendering. Behind the scenes, Kafka processes 7 trillion events per day for activity tracking, notification delivery, recommendation updates, and search indexing. The feed you see is assembled from pre-computed results that were updated by event-driven consumers.
Shopify: Webhook-based events notify merchants of orders, inventory changes, and customer actions. The storefront API is request-driven (synchronous product queries, cart operations). The backend processing (payment capture, fulfillment, tax calculation) is event-driven.
The Saga Pattern
When a business operation spans multiple services in an event-driven architecture, the saga pattern coordinates the sequence:
- Order Service creates an order and emits "OrderCreated"
- Payment Service processes payment and emits "PaymentCompleted" (or "PaymentFailed")
- Inventory Service reserves stock and emits "StockReserved" (or "StockInsufficient")
- On failure at any step, compensating events undo previous steps ("RefundPayment", "CancelOrder")
This replaces the ACID transaction that a monolith would use. The tradeoff: more code, more failure modes, but full service independence.
Side-by-Side Comparison
| Dimension | Request-Driven | Event-Driven |
|---|---|---|
| Coupling | Tight (caller knows callee) | Loose (producer ignores consumers) |
| Latency | Immediate response | Eventual processing |
| Error Handling | Synchronous, simple | Async, complex (DLQ, retries) |
| Debugging | Stack traces | Distributed tracing |
| Scaling | Connection-limited | Queue-buffered |
| Data Consistency | Strong (single request) | Eventual |
| Adding Consumers | Requires caller changes | Zero producer changes |
The default for user-facing APIs is request-driven. The default for inter-service communication and background processing is event-driven. The art is choosing the boundary correctly: synchronous where the user needs immediate feedback, asynchronous everywhere else.
Practical Implementation for .NET Developers
In a .NET application, you would typically implement this pattern using the following approach:
ASP.NET Core setup: Create a service class that encapsulates the logic, register it with dependency injection, and inject it into your controllers or minimal API endpoints. The built-in DI container handles lifecycle management.
Entity Framework Core: For database interactions, EF Core provides the ORM layer. Use migrations for schema management and raw SQL for performance-critical queries. Consider Dapper for read-heavy paths where EF Core's overhead matters.
Azure integration: If deploying to Azure, leverage managed services — Azure Cache for Redis, Azure SQL, Azure Service Bus, Azure Cosmos DB. These eliminate operational overhead and provide built-in monitoring through Application Insights.
Testing: Use xUnit with Testcontainers for integration tests that spin up real databases in Docker. Mock external dependencies with NSubstitute. The WebApplicationFactory class lets you test your entire HTTP pipeline in-process.
Monitoring: Add Application Insights telemetry to track request latency, dependency calls, and custom metrics. Use structured logging with Serilog to make production debugging possible:
Log.Information("Processing order {OrderId} for {CustomerId}", orderId, customerId);
This gives you searchable, structured logs in Azure Monitor or Seq.