Event-driven architecture is useful, but not every interaction between services should be event-driven.
That is an important point.
Once a team starts using events, it can be tempting to use them everywhere.
A service needs data from another service?
Publish an event.
A workflow needs to trigger an action?
Publish an event.
A user clicks a button?
Publish an event.
But events are not automatically better than APIs.
They solve a different problem.
Good architecture is not about choosing events or synchronous calls as a religion. It is about choosing the communication style that matches the business need.
A synchronous API call is direct.
One service asks another service for something and waits for the answer.
For example:
Order Service calls Payment Service
Payment Service responds with success or failure
The caller usually needs the answer before it can continue.
An event is different.
A service publishes that something happened.
Other services may react later.
For example:
Order Service publishes OrderCreated
Payment Service reacts when it receives the event
Inventory Service reacts when it receives the event
Email Service reacts when it receives the event
The publisher does not wait for all consumers to finish.
That difference affects coupling, latency, failure handling, user experience, and consistency.
Synchronous APIs are a good fit when the caller needs an answer now.
For example:
Can this user access this resource?
Is this token valid?
What is the current price?
Is this product in stock?
Can this payment method be used?
These are questions.
The caller cannot continue safely without the answer.
For example, if a user wants to access an admin page, the system needs to know whether the user is allowed.
An event like this would not make sense:
AccessCheckRequested
if the user is waiting for the page to load.
The application needs a direct answer.
In that case, a synchronous API call is often the simplest and clearest choice.
Events are a good fit when a business fact has occurred and other systems may want to react.
For example:
OrderCreated
PaymentSucceeded
UserRegistered
SubscriptionCancelled
InvoiceGenerated
GameRoundCompleted
These are facts.
The service that publishes the event is not asking one specific service for an immediate answer.
It is announcing that something happened.
Other systems can react independently:
Send an email
Update analytics
Start a workflow
Update a projection
Sync to a CRM
Trigger fraud analysis
This is where events are powerful.
The producer does not need to know every downstream consumer.
A useful question is:
Do I need an answer now, or am I announcing that something happened?
If you need an answer now, a synchronous API may be better.
If you are announcing that something happened, an event may be better.
Another useful question:
Is this a request or a fact?
A request often maps to an API call or command.
A fact often maps to an event.
For example:
Request: Can this user access this page?
Fact: UserLoggedIn
Request: Can this payment be authorized?
Fact: PaymentAuthorized
Request: What is the current balance?
Fact: BalanceUpdated
This distinction keeps the architecture easier to reason about.
One mistake is using events for things that are really queries.
For example:
UserProfileRequested
ProductPriceRequested
AccessCheckRequested
These messages ask for information.
If the caller is waiting for the result, using events can make the system unnecessarily complicated.
Now you need:
A request event
A response event
A correlation mechanism
A timeout
A retry policy
A place to wait for the result
This can be useful in some advanced cases, but for many systems it is overkill.
A normal synchronous API call may be clearer:
GET /users/{id}
GET /products/{id}/price
POST /access/check
Events are great for facts and asynchronous reactions.
They are not always the best tool for request-response queries.
The opposite mistake is using synchronous APIs for every downstream reaction.
Imagine an Order Service creates an order.
Then it directly calls:
Payment Service
Inventory Service
Email Service
Analytics Service
CRM Service
Recommendation Service
Now order creation depends on all those services.
If Analytics is down, should order creation fail?
Probably not.
If Email is slow, should the user wait?
Probably not.
If CRM integration has a temporary outage, should the customer be blocked from buying?
Probably not.
This is a good place for events.
The Order Service should publish:
OrderCreated
Then other services can react independently.
Synchronous calls are useful when you need an answer.
They become harmful when they create unnecessary runtime coupling.
Synchronous APIs create runtime coupling.
If Service A calls Service B, then Service B needs to be available and responsive at that moment.
That is not always bad.
Sometimes it is exactly what you need.
But it has consequences.
For example:
If Service B is slow, Service A is slow.
If Service B is down, Service A may fail.
If Service B has high latency, the user may feel it.
Events reduce some of that runtime coupling.
The producer can publish an event and continue.
Consumers can process later.
If the Email Service is down for a few minutes, it can catch up when it recovers.
That makes events useful for asynchronous work.
But events do not remove coupling completely.
They move the coupling to the event contract.
Consumers now depend on the meaning and schema of the event.
That coupling is looser, but it still exists.
Synchronous calls affect user experience directly.
If a user action waits for five service calls, the user waits for all of them.
For example:
Create order
Call payment
Call inventory
Call email
Call analytics
Return response
This can become slow and fragile.
A better design might be:
Create order
Return confirmation that order is being processed
Publish OrderCreated
Process downstream steps asynchronously
But this depends on the product.
Sometimes the user needs a final answer before moving on.
For example, a checkout page may need to know whether payment was authorized.
In that case, asynchronous processing may still happen later, but the core payment decision may need to happen before the user sees success.
The key is to decide what the user actually needs to know immediately.
Consistency is another important factor.
If a service needs the latest state before making a decision, a synchronous call to the owning service may be appropriate.
For example:
Does this account have enough balance?
Is this user currently allowed to access this feature?
Is this inventory item still available?
Using an eventually consistent projection may be risky if the decision needs fresh data.
On the other hand, if the data can lag slightly, an event-driven projection may be better.
For example:
Reporting dashboard
Search index
Recommendation model
Email personalization
Analytics view
These systems often do not need perfectly fresh data.
They can be updated through events.
A good architecture separates decisions that need strong consistency from views that can be eventually consistent.
A service should own the decisions for its domain.
For example:
Payment Service owns payment authorization.
Inventory Service owns stock reservation.
User Service owns user identity.
Billing Service owns invoices.
If another service needs a decision from that domain, it should ask the owning service.
That may be a synchronous API call or a command, depending on the workflow.
But the decision should not be duplicated everywhere.
For example, the Order Service should not copy all payment rules locally just to avoid calling the Payment Service.
Events can keep services informed, but they should not always replace domain ownership.
A service can maintain a local projection of another service's data, but that projection may be stale.
That is fine for some use cases.
It is dangerous for others.
One common design choice is whether to call another service live or keep a local projection.
For example, the Order Service may need customer information.
Option one:
Order Service calls User Service when needed
Option two:
User Service publishes UserUpdated
Order Service keeps a local customer projection
The API call gives fresher data but creates runtime dependency.
The projection reduces runtime dependency but may be stale.
Which is better?
It depends.
For example, shipping address at checkout may need to be current.
But customer segment for analytics can probably be eventually consistent.
Ask:
How fresh does this data need to be?
What happens if it is stale?
Can the owner service be unavailable?
Is this used for a critical decision or just display?
That usually reveals the right choice.
Events are often a good fit for side effects.
For example, after a user registers:
Send welcome email
Create CRM contact
Update analytics
Start onboarding flow
The User Service should not need to synchronously call every system.
It can publish:
UserRegistered
Then each consumer reacts.
This keeps the registration flow simpler and more resilient.
If the CRM integration is down, registration can still succeed.
The CRM consumer can retry later.
That is a good use of events.
The user's registration should not depend on CRM being online.
APIs are often a good fit for validation that must happen before an action succeeds.
For example:
Check whether a coupon is valid
Check whether a payment method is allowed
Check whether the user has permission
Check whether a username is available
These are immediate decisions.
If the decision is required before completing the user action, a synchronous API can be clearer.
You could model validation asynchronously, but then the user experience changes.
Instead of:
Your order is confirmed
the user may see:
Your order is being validated
That may be fine in some domains.
In others, it is not.
The architecture should match the product experience.
Sometimes the choice is not just API versus event.
There is also the command model.
A command asks a service to do something:
ReserveInventory
AuthorizePayment
CreateInvoice
CancelSubscription
A command can be sent synchronously or asynchronously.
For example, an orchestrator may send an asynchronous command:
ReserveInventory
The Inventory Service later publishes:
InventoryReserved
This is different from publishing a pure event like:
OrderCreated
The command has one intended handler.
The event may have many subscribers.
This matters because not all message-based communication is event-driven.
Some of it is asynchronous command processing.
That can be a good design, especially in sagas.
The important thing is to be clear about the meaning of the message.
Sometimes teams implement request-response over a message broker.
For example:
Service A publishes RequestCustomerData
Service B consumes it
Service B publishes CustomerDataReturned
Service A waits for response
This can work.
But it adds complexity.
You need:
Correlation IDs
Reply topics or queues
Timeout handling
Retry behavior
Duplicate response handling
Operational visibility
If the caller is waiting for a response anyway, a normal API call may be simpler.
Messaging-based request-response can be useful when the caller and receiver cannot communicate directly, when buffering is needed, or when the infrastructure is designed around messaging.
But I would not use it by default.
Do not turn a simple API call into a distributed workflow unless the problem requires it.
Synchronous APIs and events fail differently.
With a synchronous API call, failure is immediate.
For example:
Payment Service is unavailable
Order Service receives an error
Order creation fails or returns pending state
This can be good because the caller knows the result immediately.
But it also means the caller is directly affected by the dependency.
With events, failure is delayed.
For example:
OrderCreated is published
Payment consumer fails later
Message is retried
Order remains PendingPayment
This can make the system more resilient, but it also means you need clear states and observability.
The user may not get an immediate final answer.
Neither model is always better.
The right choice depends on how the business wants failure to behave.
One of the best design questions is:
What should happen if the dependency is down?
If the answer is:
The user action must fail immediately.
then a synchronous call may be appropriate.
If the answer is:
The user action should succeed and this work can happen later.
then an event may be better.
For example:
If the payment provider is down, checkout probably cannot complete.
If analytics is down, checkout should still complete.
If email is down, registration should still complete.
If access control is down, protected access may need to fail closed.
This question connects architecture to business behavior.
That is where the decision should come from.
A distributed monolith happens when services are separated physically but still tightly coupled operationally.
For example:
Service A cannot work unless Service B, C, D, and E are all available.
Every user request triggers a chain of synchronous calls.
A small downstream failure breaks the whole flow.
Deployments require many services to change together.
This is one of the risks of using APIs everywhere.
Events can reduce this coupling for asynchronous reactions.
But events can also create a distributed monolith if every service depends on every event in fragile ways.
The real goal is not just to use events.
The real goal is to create clear ownership, clear contracts, and failure boundaries that make sense.
When choosing between a synchronous API and an event, I like to ask:
Does the caller need an immediate answer?
Is this a command, a query, or a fact?
Can the work happen later?
Can the user continue before this finishes?
What happens if the receiver is down?
How fresh does the data need to be?
Is this a critical business decision?
Are multiple consumers interested?
Would adding a new consumer require changing the producer?
How will failures be observed and retried?
These questions are more useful than starting with a technology choice.
The communication style should follow from the business requirement.
When a user registers, the core action is creating the user account.
The system may also need to:
Send welcome email
Update analytics
Create CRM contact
Start onboarding sequence
The User Service should probably not synchronously depend on all of those.
A good design might be:
Create user
Publish UserRegistered
Return success
Then other services react.
If email fails, it can retry.
If CRM is down, it can catch up later.
The user should not be blocked because analytics is unavailable.
This is a good event-driven use case.
Payment authorization is different.
If the user is checking out, the system may need to know whether payment was authorized before confirming the order.
This may require a synchronous call to the Payment Service or payment provider.
For example:
Authorize payment
Receive success or failure
Continue checkout flow
After payment succeeds, the Payment Service can publish:
PaymentAuthorized
Other systems can react asynchronously.
So the design can use both:
Synchronous call for the critical decision
Event for downstream reactions
That is common.
The choice is not always either-or.
Reporting is often a good fit for events.
For example:
OrderCreated
PaymentSucceeded
SubscriptionCancelled
RefundIssued
A Reporting Service can consume these events and build read models.
It does not need to synchronously call every source service for every dashboard view.
The reporting data may lag slightly, but that is often acceptable.
If the Reporting Service is down, the core business flow can continue.
When it recovers, it can process the backlog.
This is a good use of asynchronous event processing.
Access control is usually different.
If a user tries to access a protected resource, the system needs to decide immediately.
Using a stale projection of permissions may be dangerous.
For example, if an admin removed access, the system should not allow the user in just because a local projection has not caught up yet.
In this case, a synchronous call to the authority that owns permissions may be better.
Or the system may use a local token or cached permission model with clear expiration rules.
The important point is that access decisions often have stronger consistency requirements.
They should not automatically be treated like analytics or reporting.
Real systems often use a hybrid approach.
For example, checkout might work like this:
Synchronous:
- Validate basket
- Check inventory availability
- Authorize payment
Asynchronous:
- Send confirmation email
- Update analytics
- Sync CRM
- Build reporting projection
- Trigger recommendation updates
This is normal.
A good architecture uses synchronous communication for decisions that must happen now and events for facts that other systems can react to later.
The goal is not architectural purity.
The goal is a system that behaves correctly and is operable.
If I had to explain how to choose between synchronous APIs and events in an interview, I would say:
I use synchronous APIs when the caller needs an immediate answer, especially for queries or decisions that must happen before the user action can continue. Examples are access checks, current price lookups, payment authorization, or validation that must be strongly consistent.
I use events when something has already happened and other systems may need to react asynchronously. Examples are
OrderCreated, PaymentSucceeded, UserRegistered, or
SubscriptionCancelled. Events are useful when multiple consumers may care and the producer should
not be tightly coupled to all of them.
The trade-off is that synchronous APIs create runtime coupling, because the caller depends on the receiver being available and fast. Events reduce that runtime coupling, but introduce eventual consistency, retries, duplicate messages, and more operational complexity.
I usually ask what should happen if the dependency is down. If the user action must fail or wait, a synchronous call may be appropriate. If the work can happen later, an event is often a better fit. In many real systems, I would use both: synchronous calls for critical decisions, and events for downstream reactions.
Events are powerful, but they are not a replacement for every API call.
Synchronous APIs are not old-fashioned, and events are not automatically more scalable or more elegant.
They solve different problems.
Use APIs when you need an answer now.
Use events when you are publishing a fact and other systems can react later.
Use commands when you want a specific service to perform an action.
And when the decision is not obvious, ask the business question:
What should happen if this dependency is slow, unavailable, or delayed?
That question usually reveals the right architecture.
This post is part of my Backend Architecture Notes series. In the next post, I will look at how to design event payloads, including metadata, naming, identifiers, and what should or should not be included in an event.