Architecture guide

Synchronous calls vs events: how should services talk?

Events are not the mature choice and synchronous calls are not the naive one. Making everything asynchronous produces a system where no single place knows whether a request succeeded, which is harder to operate than the coupling it removed. The decision is per interaction, and it turns on one question: does the caller need the answer before it can reply to the user.

When is a synchronous call the right choice?

When the caller cannot continue without the result. Checking whether a card authorises, confirming a seat is still free, validating a token: the user is waiting and the answer changes what happens next. Turning these into events produces a worse system, because you have to invent a way to tell the user later what could have been said immediately.

Synchronous is also right when the operation is a query. Events are for facts that have happened, not for asking questions, and a great deal of accidental complexity comes from teams that publish a request event and subscribe to a response event to avoid making an HTTP call.

The honest constraint is depth. One synchronous call is fine. A chain of five, where each service calls the next and waits, is where the availability arithmetic turns against you, and the fix is usually to restructure the work rather than to make the same chain asynchronous.

What does a chain of synchronous calls cost?

Availability multiplies and latency adds. If a request needs five services and all five must respond, the ceiling on that request's success rate is the product of the five, so a chain is always less reliable than its least reliable link. Latency behaves worse than the average suggests, because the user experiences the slowest response in the chain, and the slow tail of each service is what shows up.

Under load this becomes a cascade. A slow downstream service holds the caller's connections open, the caller's thread or connection pool fills, and the caller starts failing requests that had nothing to do with the slow dependency. The failure travels upstream faster than the load did, which is why the outage looks like it started everywhere at once.

The countermeasures are unglamorous and non-optional: a timeout on every outbound call that is shorter than the caller's own deadline, a bounded connection pool so one dependency cannot consume all capacity, a circuit breaker that stops calling a dependency that is clearly down, and retries with jitter and a strict budget. Retries without a budget turn a brief wobble into a self-inflicted denial of service.

What are the four ways services can talk?

Request and response, asynchronous command, event notification, and event-carried state transfer. They are frequently confused, and most of the arguments about 'event-driven architecture' are really arguments between the last two.

PatternSender knowsBest forMain hazard
Request and responseThe receiver, and whether it workedQueries and decisions the user is waiting onAvailability multiplies down the chain
Asynchronous commandThe receiver, not the outcomeWork that can finish later: emails, exports, indexingReporting failure back to a user who has gone
Event notificationNothing about consumersAnnouncing a fact so others can reactConsumers call back for detail, recreating coupling
Event with stateNothing about consumersFeeding read models and cross-service filteringContract is now the payload, and it is public

What do events actually buy you?

Temporal decoupling and unknown consumers. The publisher does not need the consumer to be running, so a downstream outage becomes a queue depth rather than a failed user request, and a new consumer can be added later without the publisher changing at all. For anything that does not need to happen before the response, this is a genuine and large win.

What events do not buy is loose coupling in general. A consumer that depends on a field in your event is coupled to you exactly as firmly as one that calls your API, and the coupling is harder to see because there is no call site to search for. Publishing an event whose payload mirrors your internal database rows is the same as sharing your schema, with a broker in between.

So the useful discipline is treating the event payload as a published interface designed for consumers, versioned, additive-only, and separate from your internal model. Teams that skip this discover that a rename in their own database breaks three other services.

What breaks once you go asynchronous?

Delivery guarantees, and everything downstream of them. Practically every broker offers at-least-once delivery, which means duplicates are normal operation rather than an incident, so every consumer needs to be idempotent. The usual mechanism is a natural key or a message id recorded in the same transaction as the effect, so a repeat is recognised and ignored.

Ordering is the next surprise. Messages are ordered within a partition and not across partitions, so if you need per-customer ordering, the customer id must be the partition key. Teams that discover this after launch find their events applying out of sequence for exactly the customers who are busiest.

Then poison messages and the dead letter queue. A message that always fails will retry forever and block a partition unless it is moved aside, and the dead letter queue needs a person, an alert and a replay path. A dead letter queue nobody looks at is a silent data loss mechanism with a comforting name.

How do you decide for a given interaction?

Ask whether the user is waiting on this specific result. If yes, call synchronously and put a timeout on it. If no, publish or enqueue. That one question settles most cases correctly, and the ones it does not settle are worth arguing about individually rather than resolving with a policy.

Then ask whether you are telling someone a fact or asking them to do something. Facts are events, named in the past tense, with no expectation of a particular consumer. Instructions are commands, addressed to one receiver, and pretending a command is an event by naming it in the past tense fools nobody and hides the dependency.

A useful check on an existing system: draw the calls made during your most important user action and mark each one as blocking or not. Any blocking call whose result does not change the response is a candidate for moving off the request path immediately, and that change usually improves both latency and availability more than any other single intervention.

Common questions

Should microservices communicate synchronously or with events?
Per interaction, decided by whether the caller needs the result to respond. Card authorisation, availability checks and token validation stay synchronous because the answer changes what happens next. Work that can complete later, such as sending email, updating a search index or generating an export, belongs on a queue or an event. Applying one style everywhere causes more problems than mixing them.
Why are long chains of synchronous service calls a problem?
Availability multiplies and latency adds. When five services must all respond, the request's success rate is bounded by the product of theirs, and the user experiences the slowest link rather than the average. Under load a slow dependency holds connections open until the caller's pool is exhausted, so failure spreads upstream. Timeouts, bounded pools, circuit breakers and a retry budget are the minimum defences.
What is the difference between an event and a command?
A command instructs one specific receiver to do something and the sender cares that it happens. An event announces a fact that has already occurred, is named in the past tense, and the publisher has no expectation about who consumes it or whether anyone does. Naming a command in the past tense to make it look like an event hides a direct dependency rather than removing it.
Do events actually reduce coupling between services?
Only temporally. A consumer that reads a field from your event is coupled to that field exactly as tightly as one calling your API, and the dependency is harder to find because there is no call site. Events remove the requirement that both services are running at the same moment. Keeping coupling manageable requires treating the payload as a versioned public contract distinct from your internal schema.
Why do event consumers need to be idempotent?
Because brokers deliver at least once, so the same message arriving twice is normal operation rather than a fault. A consumer that is not idempotent will charge a card twice or create duplicate records during any redelivery. The usual implementation records the message id or a natural key in the same transaction as the effect, so a repeat is recognised and discarded.

More on Microservices architecture

Let’s create something out of this world together.

Have a project in mind? Contact us for expert design and development solutions. Let’s discuss how we can help grow your business.

Azaadi Offer

Claim a free security assessment

Until 31 August we're covering the cost of a full vulnerability assessment and penetration test. Mention it in your message and we'll scope it with you.

  • Web application testing, authenticated and unauthenticated
  • Mobile application testing across iOS and Android
  • External network and infrastructure assessment
  • Manual exploitation by engineers, not scanner output

Testing and the report are free. Fixing what we find is quoted separately, with no obligation to accept.

Read the full offer

Tell us what you are trying to build and we will tell you plainly whether we are the right people for it. Book a call with an expert to work through the detail, or ask for a fixed quote if the scope is already clear. No obligation either way.

Four fields is all we need to get started.

Fastnexa Logo

© 2026 fastnexa. All rights reserved.