Event-driven architecture is often presented as the natural next step for systems that need to scale. Replace direct service calls with queues, publish events, add consumers, and suddenly the architecture appears more resilient and flexible.
In practice, asynchronous systems introduce a different class of complexity.
Messages can arrive twice. Consumers can fail halfway through processing. Events can be delivered out of order. A workflow that previously existed inside a single request may now be distributed across several services, queues, retries, and databases.
None of this makes event-driven architecture a bad choice. It simply means that asynchronous communication should solve a specific problem.
This article explains when request-response communication is enough, when asynchronous messaging becomes valuable, and what operational costs appear once events become part of the architecture.
- Request-Response Should Usually Be the Default
- What Changes When You Go Asynchronous?
- Commands, Events, and Messages Are Not the Same Thing
- Command
- Event
- Message
- The Real Benefits of Event-Driven Architecture
- 1. Decoupling Service Availability
- 2. Absorbing Traffic Spikes
- 3. Moving Slow Work Outside the User Request
- 4. Independent Consumer Scaling
- The Price: Eventual Consistency
- Retries Change Application Semantics
- Idempotency Is Not Optional
- Exactly-Once Delivery Is Usually the Wrong Mental Model
- Ordering Is More Expensive Than It Looks
- Dead-Letter Queues Are Part of the Design
- The Dual-Write Problem
- The Transactional Outbox Pattern
- Event Choreography vs Workflow Orchestration
- Choreography
- Orchestration
- Do Not Turn Every Service Call Into an Event
- Queue vs Pub/Sub
- Queue
- Publish-Subscribe
- When Do You Need an Event Stream?
- Observability Becomes More Important
- Consumer Lag Is Often More Important Than Queue Size
- Schema Evolution Becomes an Architecture Problem
- Event Replay Is Powerful — and Dangerous
- When Event-Driven Architecture Makes Things Worse
- Too Many Small Services
- Invisible Dependencies
- Harder Debugging
- Operational Overhead
- Eventual Consistency Everywhere
- Complex Recovery
- Distributed Transactions Become Business Workflows
- A Practical Decision Framework
- Five Signals That You May Actually Need Async Messaging
- 1. Non-critical downstream failures are breaking user requests
- 2. Traffic arrives faster than downstream systems can process it
- 3. Users are waiting for operations that do not need to finish immediately
- 4. Many independent systems react to the same business change
- 5. Different parts of the workflow need independent scaling
- Five Signs You Are Introducing Events Too Early
- 1. The system has very little traffic
- 2. Every event has exactly one producer and one consumer
- 3. Developers cannot explain the failure model
- 4. The business requires immediate consistency
- 5. The architecture exists mainly because a messaging technology is fashionable
- A Better Migration Path
- Step 1: Keep Core Transactions Synchronous
- Step 2: Move Non-Critical Side Effects to Background Processing
- Step 3: Add Idempotency and Retry Handling
- Step 4: Add Dead-Letter Handling and Operational Metrics
- Step 5: Introduce Domain Events Where Multiple Consumers Need Them
- Step 6: Add More Sophisticated Streaming Infrastructure Only When Required
- A Simple Architecture Is Often a Hybrid Architecture
- Final Takeaway
Request-Response Should Usually Be the Default
Most applications begin with synchronous communication.
A client sends a request. The server performs some work. The server returns a response.
Client
|
| POST /orders
v
Order Service
|
| response
v
Client
The model is simple because the entire operation has a clear beginning and end.
If the request succeeds, the caller receives a success response. If something fails, the caller receives an error.
This simplicity has significant operational value.
- Failures are easier to understand.
- Logs usually follow a single request path.
- Debugging is relatively straightforward.
- Users receive immediate feedback.
- Consistency is easier to reason about.
- Developers need fewer infrastructure components.
For many systems, this model can scale much further than teams initially expect.
A synchronous architecture does not automatically mean a tightly coupled monolith. Independent services can still expose APIs and communicate over HTTP or RPC.
The important question is not whether events are more modern.
The question is whether synchronous dependencies are creating a measurable limitation.
What Changes When You Go Asynchronous?
In an asynchronous architecture, the producer does not necessarily wait for the consumer to complete its work.
Order Service
|
| OrderCreated
v
Message Broker
/ | \
v v v
Email Stock Analytics
The order service can publish an event and continue.
Other components process that event independently.
This changes the relationship between services.
The producer no longer needs every consumer to be available at the exact moment the event is created. Consumers can process messages later, retry failures, or scale independently based on queue depth.
This property is often called temporal decoupling.
It is one of the strongest reasons to introduce asynchronous messaging.
Commands, Events, and Messages Are Not the Same Thing
These terms are often used interchangeably, but they describe different intentions.
Command
A command asks another component to perform an action.
ChargePayment
GenerateInvoice
SendEmail
Commands are usually directed toward a specific handler.
Event
An event describes something that has already happened.
OrderCreated
PaymentCompleted
UserRegistered
The publisher does not necessarily know which systems will consume the event.
Message
Message is the broader transport concept. A message may contain a command, event, notification, or another unit of data.
Keeping these concepts separate makes distributed workflows easier to understand.
The Real Benefits of Event-Driven Architecture
Event-driven systems become useful when asynchronous communication solves a specific architectural constraint.
The most common constraints are availability dependencies, traffic spikes, long-running work, independent scaling, and integration between multiple downstream systems.
1. Decoupling Service Availability
Consider an order workflow.
Order Service
|
+--> Payment Service
|
+--> Inventory Service
|
+--> Email Service
|
+--> Analytics Service
If all calls happen synchronously, the order request may depend on every downstream service being healthy.
This creates an availability chain.
The email service should probably not determine whether an order can be accepted. Neither should the analytics platform.
Event-driven communication allows non-critical work to happen independently.
Order Service
|
| OrderCreated
v
Message Broker
|
+--> Email Consumer
+--> Analytics Consumer
+--> Fulfillment Consumer
If the analytics consumer is temporarily unavailable, order creation can continue.
The events remain available for later processing.
This is a meaningful reduction in coupling.
2. Absorbing Traffic Spikes
Synchronous systems often require downstream services to handle incoming traffic at approximately the same rate as the upstream system.
That becomes difficult when traffic arrives in bursts.
Imagine a service normally processing 500 jobs per minute but occasionally receiving 20,000 jobs during a short traffic spike.
Without buffering, the downstream service must either scale immediately or begin rejecting requests.
A queue changes the model.
Incoming Requests
|
v
Queue
|
|
v
Worker Pool
Producers can continue accepting work while consumers process the backlog at a sustainable rate.
This is often called load leveling.
Queue depth then becomes an important scaling signal.
Instead of scaling workers only from CPU utilization, the platform can scale based on the amount of pending work.
3. Moving Slow Work Outside the User Request
Some operations simply do not belong in the synchronous request path.
Examples include:
- video encoding,
- PDF generation,
- image processing,
- large data imports,
- report generation,
- email delivery,
- machine learning inference pipelines,
- bulk synchronization with external systems.
Keeping these operations inside an HTTP request can lead to long timeouts and poor user experience.
A better design is often:
POST /reports
|
v
Create Job
|
v
Queue
|
v
Report Worker
|
v
Object Storage
The API can immediately return a job identifier while background workers complete the expensive operation.
4. Independent Consumer Scaling
Events allow different workloads to scale independently.
Suppose every completed purchase generates three types of downstream processing:
- fraud analysis,
- customer notifications,
- analytics aggregation.
These workloads may have completely different resource requirements.
Fraud analysis might be CPU-intensive. Email delivery might be network-bound. Analytics may be optimized for batch processing.
Independent consumers allow each workload to scale according to its own needs.
The Price: Eventual Consistency
The largest conceptual change in asynchronous architecture is often not the message broker.
It is consistency.
In a synchronous workflow, an application may update several operations before returning success.
In an event-driven system, downstream state may appear later.
For example:
Order Created
|
| 0 ms
v
Order Database Updated
|
| +100 ms
v
Inventory Updated
|
| +500 ms
v
Search Index Updated
|
| +2 sec
v
Analytics Updated
During that window, different systems can contain different versions of reality.
This is eventual consistency.
For many business processes, that is perfectly acceptable.
For others, it is not.
A payment authorization may need stronger guarantees than an analytics dashboard.
Architecture should reflect those differences rather than applying one consistency model everywhere.
Retries Change Application Semantics
Reliable message processing usually depends on retries.
If a consumer fails while processing a message, the broker may deliver that message again.
This sounds straightforward until the operation has side effects.
Imagine the following message:
{
"type": "PaymentRequested",
"orderId": "ORD-10482",
"amount": 99.00
}
The consumer charges the customer successfully but crashes before acknowledging the message.
The broker delivers the message again.
Without protection, the customer may be charged twice.
This is why asynchronous systems need to assume duplicate delivery can happen.
Idempotency Is Not Optional
An idempotent operation can safely be executed multiple times without changing the final result beyond the first successful execution.
One common pattern is to attach a unique operation identifier to each message.
{
"eventId": "evt_7f9d82",
"type": "PaymentRequested",
"orderId": "ORD-10482"
}
Before performing the operation, the consumer checks whether that event has already been processed.
if event_already_processed(eventId):
acknowledge_message()
return
process_event()
store_processed_event(eventId)
acknowledge_message()
The exact implementation varies, but the architectural principle remains the same:
Design consumers as if every message may arrive more than once.
Exactly-Once Delivery Is Usually the Wrong Mental Model
Teams sometimes try to solve duplicate processing by searching for infrastructure that promises exactly-once delivery.
The problem is that delivery guarantees and business side effects are not the same thing.
A message platform may provide strong delivery guarantees inside its own boundaries, but your consumer may also write to a database, call an external API, charge a credit card, or upload a file.
Those side effects live outside the broker.
For that reason, robust distributed systems usually combine infrastructure guarantees with idempotent application logic.
Ordering Is More Expensive Than It Looks
Many event-driven designs begin with the assumption that events will be processed in the same order in which they were created.
Distributed processing can break that assumption.
Event 1: CustomerAddressUpdated
Event 2: CustomerAddressUpdated
Event 3: CustomerDeleted
With multiple consumers, retries, partitions, and network delays, the processing order may differ from the production order.
Before requiring strict ordering, ask whether the business process actually needs it.
Global ordering can severely limit parallelism.
A more scalable approach is often to require ordering only within a logical entity:
- one customer,
- one account,
- one order,
- one device.
That allows unrelated entities to be processed concurrently.
Dead-Letter Queues Are Part of the Design
Some messages will repeatedly fail.
The reason may be invalid data, a permanent downstream error, an application bug, or an event schema the consumer does not understand.
Retrying forever is rarely useful.
A common pattern is:
Main Queue
|
v
Consumer
|
| failure
v
Retry
|
| repeated failure
v
Dead-Letter Queue
A dead-letter queue is not simply a place where broken messages disappear.
It needs an operational process around it.
Teams should know:
- who monitors it,
- what alerts are triggered,
- how messages are inspected,
- how data is corrected,
- how messages are safely replayed.
A dead-letter queue without ownership is just a delayed production incident.
The Dual-Write Problem
One of the most important failure modes in event-driven architecture appears when an application updates its database and publishes an event as two separate operations.
1. INSERT order
2. Publish OrderCreated event
What happens if the database write succeeds but the event publication fails?
The order exists, but downstream systems never hear about it.
Reversing the order creates the opposite problem.
1. Publish OrderCreated event
2. INSERT order
If the event succeeds but the database transaction fails, consumers may process an order that does not exist.
This is the dual-write problem.
The Transactional Outbox Pattern
A common solution is the transactional outbox.
Instead of publishing directly to the broker, the application stores the event in the same database transaction as the business change.
BEGIN TRANSACTION
INSERT INTO orders (...)
INSERT INTO outbox (
event_type,
payload
)
COMMIT
A separate publisher then reads the outbox and sends events to the messaging system.
Application
|
v
Database
| |
Orders Outbox
|
v
Publisher
|
v
Event Broker
This does not eliminate every failure mode, but it closes an important consistency gap between application state and event publication.
Event Choreography vs Workflow Orchestration
As event-driven systems grow, business workflows can be coordinated in two broad ways.
Choreography
Each service reacts to events and publishes new events.
OrderCreated
|
v
Payment Service
|
PaymentCompleted
|
v
Inventory Service
|
InventoryReserved
|
v
Shipping Service
No central component owns the entire workflow.
Choreography can work well for simple workflows with loose coupling.
But long chains can become difficult to understand.
Answering a basic question such as “why has this order not shipped?” may require tracing events across multiple services.
Orchestration
A dedicated workflow component manages the sequence.
Workflow
Orchestrator
/ | \
v v v
Payment Stock Shipping
The orchestrator knows the current workflow state and tells participants what to do next.
This introduces central coordination but can make complex business processes much easier to reason about.
Do Not Turn Every Service Call Into an Event
Event-driven architecture does not mean synchronous APIs disappear.
Most mature systems use both models.
Synchronous requests are usually appropriate when:
- the caller needs an immediate answer,
- the operation is short,
- the dependency is required to complete the request,
- strong consistency is important,
- failure should be visible to the caller immediately.
Asynchronous communication is often more appropriate when:
- work can happen later,
- temporary consumer downtime should not block producers,
- traffic needs buffering,
- processing is slow or resource-intensive,
- multiple independent consumers need the same information,
- consumers need to scale independently.
Queue vs Pub/Sub
Not every asynchronous use case requires a full event streaming platform.
The first distinction is usually between work queues and publish-subscribe systems.
Queue
Producer
|
v
Queue
/ | \
v v v
Workers
A queue is usually appropriate when one unit of work should be handled by one consumer.
Examples:
- resize an image,
- generate a report,
- process an uploaded file,
- send an email.
Publish-Subscribe
Event
|
v
Broker
/ | \
v v v
A B C
Publish-subscribe is useful when multiple independent consumers need to react to the same event.
For example, an OrderCompleted event might be consumed by analytics,
loyalty, fulfillment, and notification systems.
When Do You Need an Event Stream?
Some platforms treat messages primarily as transient jobs.
Event streaming systems often treat events as an ordered, retained log that can be consumed and replayed.
That distinction matters when:
- multiple consumer groups need independent positions,
- events need long retention periods,
- historical events need to be replayed,
- high-throughput data pipelines are required,
- stream processing is a core workload.
If your requirement is simply “run this background job later,” an event streaming platform may add unnecessary operational complexity.
Use the simplest messaging model that satisfies the actual delivery and processing requirements.
Observability Becomes More Important
A synchronous request often produces a relatively clear trace:
Browser
-> API
-> Database
-> Response
In an asynchronous system, the same business transaction may look like:
API
|
v
Database
|
v
Outbox
|
v
Publisher
|
v
Broker
|
+--> Consumer A
| |
| v
| Database
|
+--> Consumer B
|
v
External API
Logs from a single service are no longer enough.
Useful observability usually requires correlation identifiers carried across the entire workflow.
Important metrics include:
- queue depth,
- oldest message age,
- consumer lag,
- processing latency,
- retry rate,
- dead-letter volume,
- consumer error rate.
CPU utilization alone tells very little about the health of an asynchronous workflow.
Consumer Lag Is Often More Important Than Queue Size
A queue containing 100,000 messages may be perfectly healthy if consumers process millions of messages per minute.
A queue containing only 500 messages may represent an incident if those messages have been waiting for two hours.
That is why message age and consumer lag often provide better operational signals than raw queue depth.
The key question is not:
How many messages are waiting?
It is:
How far behind the expected processing time are we?
Schema Evolution Becomes an Architecture Problem
Once multiple teams consume an event, changing its structure becomes similar to changing a public API.
Consider:
{
"eventType": "CustomerCreated",
"name": "Alex"
}
A producer later changes the event to:
{
"eventType": "CustomerCreated",
"firstName": "Alex",
"lastName": "Smith"
}
Older consumers may still expect the name field.
The event producer cannot safely assume every consumer will upgrade at the same time.
Event schemas therefore need compatibility rules.
Common practices include:
- adding fields instead of removing them,
- making consumers tolerant of unknown fields,
- explicit schema versioning when necessary,
- maintaining schema contracts,
- testing producer-consumer compatibility.
Event Replay Is Powerful — and Dangerous
Retained events can sometimes be replayed.
This is useful when:
- a new consumer needs historical data,
- a bug caused incorrect processing,
- an analytics model must be rebuilt,
- a downstream database needs reconstruction.
But replaying events can also repeat side effects.
Reprocessing a historical EmailRequested event should probably not send thousands
of old emails again.
The same applies to payments, notifications, external API calls, and other irreversible actions.
Replayability must therefore be designed at the consumer level.
When Event-Driven Architecture Makes Things Worse
Events are not automatically an architectural improvement.
They can make a simple system significantly harder to operate.
Too Many Small Services
A workflow that could have been a function call becomes a distributed chain across five services.
Invisible Dependencies
With APIs, dependencies are often visible in code. With events, new consumers can appear without the producer knowing they exist.
Harder Debugging
A user-facing failure may originate from an event emitted several minutes earlier.
Operational Overhead
Brokers, consumers, retries, dead-letter queues, schema management, monitoring, and replay mechanisms all need ownership.
Eventual Consistency Everywhere
Teams sometimes accept weaker consistency even where the business does not benefit from it.
Complex Recovery
Recovering from a failed asynchronous workflow may require compensating operations rather than simply rolling back a database transaction.
Distributed Transactions Become Business Workflows
In a single database, a transaction may look like:
BEGIN
Reserve Inventory
Charge Payment
Create Order
COMMIT
Once each operation belongs to an independent service, a single database transaction is no longer available.
The system may instead perform:
Create Order
|
v
Reserve Inventory
|
v
Charge Payment
|
v
Confirm Order
If charging the payment fails after inventory has been reserved, the system needs a compensating action.
Payment Failed
|
v
Release Inventory
|
v
Cancel Order
This family of patterns is often associated with sagas.
The important architectural shift is that rollback is no longer purely technical. Recovery becomes part of the business workflow.
A Practical Decision Framework
Before introducing asynchronous messaging, ask what problem it solves.
| Requirement | Request-Response | Async Messaging |
|---|---|---|
| Immediate result required | Strong fit | Usually poor fit |
| Simple CRUD workflow | Strong fit | Often unnecessary |
| Long-running processing | Risk of timeouts | Strong fit |
| Traffic spikes | Requires immediate capacity | Queue can absorb bursts |
| Consumer downtime | Can break upstream requests | Can buffer work |
| Multiple independent consumers | Requires multiple calls | Pub/Sub fits naturally |
| Strong consistency | Easier | Usually harder |
| Debugging simplicity | Better | Requires stronger observability |
| Independent scaling | Possible | Often easier |
| Replay historical work | Requires custom design | Natural in some event platforms |
Five Signals That You May Actually Need Async Messaging
1. Non-critical downstream failures are breaking user requests
If analytics, notifications, or integrations make core API requests unreliable, those operations may belong outside the synchronous path.
2. Traffic arrives faster than downstream systems can process it
A queue can separate ingestion capacity from processing capacity.
3. Users are waiting for operations that do not need to finish immediately
Background jobs can reduce response latency and timeout risk.
4. Many independent systems react to the same business change
Publishing an event may be cleaner than maintaining a growing list of synchronous integrations.
5. Different parts of the workflow need independent scaling
Separating production from consumption allows each workload to scale according to its own bottleneck.
Five Signs You Are Introducing Events Too Early
1. The system has very little traffic
If everything comfortably runs on a small deployment, scalability alone is not a strong reason to introduce messaging.
2. Every event has exactly one producer and one consumer
That may simply be an API call disguised as an event.
3. Developers cannot explain the failure model
Before adopting asynchronous processing, the team should know what happens when messages are duplicated, delayed, lost, or processed out of order.
4. The business requires immediate consistency
Introducing eventual consistency into a workflow that cannot tolerate it creates unnecessary risk.
5. The architecture exists mainly because a messaging technology is fashionable
Infrastructure should follow requirements.
The existence of an event streaming platform is not itself a reason to redesign application workflows around it.
A Better Migration Path
Moving from synchronous communication to events does not need to happen as a large rewrite.
A safer approach is incremental.
Step 1: Keep Core Transactions Synchronous
Start with the operations where immediate consistency and user feedback matter.
Step 2: Move Non-Critical Side Effects to Background Processing
Email delivery, analytics, report generation, and similar workloads are often good first candidates.
Step 3: Add Idempotency and Retry Handling
Do this before message volume becomes large enough to expose duplicate-processing bugs.
Step 4: Add Dead-Letter Handling and Operational Metrics
Do not wait until production failures prove that they are necessary.
Step 5: Introduce Domain Events Where Multiple Consumers Need Them
Events become more valuable when they represent stable business facts rather than implementation details.
Step 6: Add More Sophisticated Streaming Infrastructure Only When Required
Retention, replay, high throughput, partitioning, and independent consumer groups should be requirements, not assumptions.
A Simple Architecture Is Often a Hybrid Architecture
The choice between synchronous and asynchronous communication is not binary.
A well-designed application may use synchronous APIs for its core transaction and events for everything that can happen afterward.
Client
|
| Create Order
v
Order API
|
+--> Validate
|
+--> Store Order
|
+--> Return Success
|
v
OrderCreated Event
|
+--> Email
+--> Analytics
+--> Fulfillment
+--> Loyalty
The core user interaction remains simple.
Secondary workloads gain the resilience and scaling advantages of asynchronous processing.
This hybrid model is often more practical than attempting to make the entire application event-driven.
Final Takeaway
Event-driven architecture is not automatically more scalable, more resilient, or more modern than request-response architecture.
It trades one set of constraints for another.
You gain buffering, temporal decoupling, independent consumers, and flexible scaling.
You also gain duplicate delivery, retries, eventual consistency, schema evolution, dead-letter handling, more difficult debugging, and more complex operational recovery.
The right question is therefore not:
Should we use event-driven architecture?
A better question is:
Which part of our system has a reliability, latency, scaling, or coupling problem that asynchronous communication would actually solve?
If there is no clear answer, request-response is probably still the simpler architecture.
If there is, introduce asynchronous communication at that boundary first.
Good cloud architecture is not about maximizing the number of distributed components. It is about introducing complexity only where the requirements justify it.
