Lesson 62 of 129
Synchronous vs Asynchronous Communication
Services communicate in one of two styles. In synchronous communication, the caller sends a request and waits for the response before continuing. In asynchronous communication, the sender hands a message to an intermediary or to the receiver and continues without waiting for the receiver to process it. The choice determines coupling, failure behavior, and latency of every workflow that crosses a service boundary.
Synchronous request/response
The caller opens a connection, sends a request (HTTP/REST, gRPC), and blocks until a response or a timeout. See REST API design and gRPC and Protobuf.
Properties:
- The caller receives the result immediately, including errors.
- The protocol is simple to reason about and to debug: one request, one response, one trace.
- The receiver must be running and reachable when the call is made.
- The caller's latency includes the receiver's processing time and network time.
Asynchronous messaging
The sender publishes a message to a broker (Kafka, RabbitMQ, SQS) or a stream. One or more consumers process it later. See message queues vs pub/sub.
Properties:
- The sender needs only the broker to be available, not the consumer.
- The broker buffers messages during consumer downtime or load spikes.
- The result, if any, arrives through a later message or a status endpoint.
- Delivery is usually at-least-once, so consumers must handle duplicates (see idempotent consumers).
sequenceDiagram
participant C as Client
participant O as Order Service
participant B as Broker
participant I as Inventory Service
Note over C,I: Synchronous
C->>O: POST /orders
O->>I: reserve stock
I-->>O: reserved
O-->>C: 201 Created
Note over C,I: Asynchronous
C->>O: POST /orders
O->>B: publish OrderPlaced
O-->>C: 202 Accepted
B->>I: deliver OrderPlaced
I->>B: publish StockReservedRequest/reply over a broker
Asynchronous messaging can still implement a request/reply exchange. The requester sends a message containing a correlation ID and a reply-to address (a queue or topic it listens on). The responder processes the message and publishes the reply to that address with the same correlation ID, which the requester uses to match the reply to its pending request. The requester is no longer blocked on a connection, but if it waits for the reply before continuing, it keeps the latency dependency and most of the temporal coupling of a synchronous call.
Temporal coupling
Temporal coupling means two services must both be available at the same time for an operation to succeed. Synchronous calls have it. Asynchronous messaging removes it: the sender completes its part while the receiver is down, and the receiver catches up when it returns.
Other coupling dimensions remain in both styles:
- Schema coupling. Both sides depend on the message or API format.
- Discovery coupling. Synchronous callers need the receiver's address (see service discovery). Asynchronous senders need the broker and topic name.
- Semantic coupling. A command such as "reserve stock" makes the sender depend on the receiver's behavior even when sent asynchronously. An event such as "OrderPlaced" only reports a fact, and the sender does not care who reacts.
Availability of chained synchronous calls
If a request passes through n services in sequence and each must succeed, the availability of the chain is the product of individual availabilities, assuming independent failures:
A_chain = A1 x A2 x ... x AnFive services at 99.9% each give 0.999^5, about 99.5%. That is roughly 44 hours of downtime per year instead of about 8.8 hours for a single service. Deeper chains and lower per-service availability compound the loss.
import java.util.Locale;
public class Main {
public static void main(String[] args) {
double perService = 0.999;
for (int n : new int[] {1, 3, 5, 10}) {
double chain = Math.pow(perService, n);
double downtimeHours = (1.0 - chain) * 24.0 * 365.0;
System.out.println(String.format(Locale.ROOT,
"%2d services: %.4f availability, %.1f h/year down", n, chain, downtimeHours));
}
}
} 1 services: 0.9990 availability, 8.8 h/year down
3 services: 0.9970 availability, 26.3 h/year down
5 services: 0.9950 availability, 43.7 h/year down
10 services: 0.9900 availability, 87.2 h/year downThe calculation assumes failures are independent and that every call in the chain is required. Shared dependencies (the same database, the same availability zone) make failures correlated, and retries add load to a degraded service. A call that has a working fallback does not multiply into the product, because its failure does not fail the request.
The broker is also a dependency in the asynchronous style. A sender that publishes to a broker is exposed to the availability of the broker, not to the product of the availabilities of all consumers. Replicated brokers keep accepting messages when a node fails (see Kafka internals), but the broker is one more system to operate.
Mitigations: reduce chain depth, make non-critical calls optional with fallbacks, cache responses, and use asynchronous messaging for steps that do not need an immediate answer. See bulkhead, fallback and load shedding and circuit breaker.
Latency
In a synchronous chain, latencies add: the end-to-end time is the sum of network hops and processing times along the path, and tail latency (p99) of the slowest dependency dominates. Parallel calls reduce the sum to the maximum, but the caller still waits for the slowest.
Worked example: a request calls A, then B, then C in sequence, with typical latencies of 20 ms, 30 ms, and 50 ms. The typical end-to-end time is about 100 ms plus network hops. If B and C are independent and called in parallel after A, the time is 20 + max(30, 50) = 70 ms. The probability that at least one of several calls lands in its own slow tail grows with the number of calls: if each call exceeds its own p99 latency 1% of the time, a request making 10 such calls has about 1 - 0.99^10, roughly 10%, chance of hitting at least one slow call. Tail latency therefore worsens as fan-out grows. Deadlines and timeouts bound the damage (see timeouts, retries, backoff and jitter).
Asynchronous processing shortens the response the user sees, since the request returns after the message is durably accepted. Total time to completion may be longer because of queueing, and the client must learn the outcome through polling, a callback, or a push channel (see WebSockets, SSE and long polling).
Failure behavior
- Synchronous. A failure is visible to the caller at once, as an error or a timeout. The caller must choose between retrying, degrading, and failing. A slow receiver is more harmful than a dead one, because blocked callers hold threads and connections until the timeout fires, and this can exhaust the caller's resources and spread the failure upstream (see bulkhead, fallback and load shedding).
- Asynchronous. A down consumer does not affect the sender. Messages accumulate in the queue, and the backlog grows until the consumer recovers or the broker's storage or retention limit is reached. The consumer lag (unprocessed messages) must be monitored, since the failure is otherwise silent. Messages that repeatedly fail processing are moved to a dead-letter queue (see backpressure, retries and dead-letter queues).
Comparison
| Aspect | Synchronous | Asynchronous |
|---|---|---|
| Temporal coupling | Yes | No |
| Result availability | Immediate | Later, via message or polling |
| Failure handling | Caller sees error, retries or fails | Broker retries, dead-letter queue |
| Load spikes | Passed directly to receiver | Absorbed by broker buffer |
| Consistency | Easier to reason about | Eventual consistency |
| Debugging | Single trace | Requires correlation across messages |
| Extra infrastructure | None beyond network | Broker to operate |
| Ordering | Implicit per call | Must be designed (partition keys) |
When to use each
Use synchronous calls when:
- The caller needs the answer to continue, such as a price lookup, authorization check, or validation before responding to a user.
- The operation is a query.
- Latency is small and the dependency is highly available.
Use asynchronous messaging when:
- The work can complete after the response, such as sending email or updating a search index.
- Multiple services react to the same fact.
- Load is bursty and the receiver should process at its own pace.
- A long-running workflow spans services (see saga pattern).
Systems commonly mix both: synchronous queries for reads, asynchronous events for state changes. See also event-driven architecture.
Interview questions
- Why does temporal coupling reduce availability? The operation succeeds only when every service in the path is up at that moment, so availabilities multiply. A broker decouples this because the sender depends only on the broker.
- Can asynchronous messaging still return an immediate result to the user? The API returns
202 Acceptedwith an identifier once the message is durably stored. The client polls a status resource or receives a notification when processing finishes. - Is a synchronous call wrapped in a queue asynchronous? No. If the caller blocks waiting for a reply message, the temporal coupling and latency dependency remain.
- What must consumers of asynchronous messages handle? Duplicate delivery and, depending on the broker and partitioning, reordering.
