Synchronous vs Event-Driven Integration
A beginner-to-production walkthrough of how services actually talk to each other — why some calls wait for an answer while others fire and move on, when to reach for each style, and how companies like Netflix, Amazon, Uber, and Google mix both patterns in the systems you use every day.
Two Ways for Software to Talk — and Why We Need Both
Every time one piece of software needs something from another piece of software, it has to integrate with it — that is, establish a way to communicate, exchange data, and coordinate work. The two dominant families of integration style you will meet in almost every architecture discussion are synchronous integration and event-driven (asynchronous) integration. This guide explains both from the ground up, assuming no prior background, and takes you all the way to how companies like Netflix, Amazon, and Uber use these patterns in production.
To understand why we have two different styles at all, it helps to look at how distributed systems evolved. In the 1990s and early 2000s, most enterprise systems were built around Remote Procedure Calls (RPC) and later SOAP web services. The mental model was simple: calling another service should feel just like calling a function in your own program — you ask, you wait, you get an answer. This is the essence of synchronous communication, and it mirrors how humans are used to asking questions: you ask a colleague something and stand there until they answer.
As systems grew larger and were split into many independently deployed services (what we now call microservices), a problem emerged. If every service call blocks and waits for a reply, and services call other services which call other services, a single slow or failed component can freeze the entire chain. Engineers began borrowing an older idea from telecommunications and hardware design — message queues and the publish-subscribe pattern — to decouple services from each other in time. This gave rise to event-driven architecture (EDA), where a service announces “something happened” and moves on, without waiting for anyone to act on it.
Today, virtually no serious production system uses only one style. Instead, architects mix synchronous APIs (usually REST or gRPC) for operations that need an immediate answer, with event-driven messaging (using brokers like Apache Kafka, RabbitMQ, or Amazon SQS/SNS) for operations that can happen “eventually.” Understanding the difference — and knowing when to use which — is one of the most fundamental skills in software architecture.
Synchronous integration is like a phone call: you dial, the other person picks up, you ask your question, and you wait on the line until they answer. You cannot hang up and do something else while waiting — the conversation blocks your day until it’s done. Event-driven integration is like sending a text message or an email: you send it and go on with your life. The other person reads it and responds whenever they get to it, and you don’t sit around waiting.
1.1 How the two styles matured side by side
It’s worth understanding that synchronous and event-driven integration did not emerge as competitors; they emerged to solve genuinely different problems and have coexisted for decades. Mainframe systems in the 1970s and 80s already used batch message queues (an early ancestor of today’s event-driven systems) to move data between departments overnight, while terminal-to-mainframe interactions were synchronous by necessity, since a human was sitting there waiting for a screen to refresh.
The rise of Service-Oriented Architecture (SOA) in the 2000s formalised enterprise messaging with technologies like IBM MQ and JMS (Java Message Service), giving developers a standard vocabulary for asynchronous, message-based integration even before “microservices” was a common term. When Netflix, Amazon, and LinkedIn popularised microservices in the 2010s, they also popularised modern open-source event streaming platforms — most notably Apache Kafka, originally built at LinkedIn in 2011 specifically to handle the volume of activity-tracking events a large social platform generates. Kafka’s design (a durable, replayable, partitioned log) became the backbone of what we now call event-driven architecture at scale.
Meanwhile, synchronous communication kept evolving too: SOAP gave way to leaner REST APIs in the mid-2000s, and REST has more recently been complemented by gRPC (Google’s high-performance RPC framework, released in 2015) for internal service-to-service calls where speed and strongly typed contracts matter more than human-readability. Both synchronous and event-driven integration are therefore mature, well-understood, actively-evolving parts of the modern architect’s toolkit — not a case of one replacing the other.
1.2 A quick history-of-integration timeline
1970s — Batch message queues on the mainframe
Departments swap files and messages between overnight batch jobs; the first ancestor of today’s asynchronous, queue-based integration.
1990s — RPC & SOAP web services
Distributed calls modelled after local function calls; simple mental model but tightly couples caller and callee in time.
2000s — SOA, IBM MQ, JMS
Enterprise messaging is formalised: durable queues, publish-subscribe, transactional semantics — async integration graduates from “special case” to first-class citizen.
2011 — Apache Kafka is open-sourced at LinkedIn
A durable, replayable, partitioned commit log becomes the default backbone for modern event-driven architectures at internet scale.
Mid-2010s — REST everywhere, then gRPC
REST wins for public APIs; gRPC (2015) becomes the internal high-throughput synchronous protocol of choice for microservice fleets.
Today — Hybrid is the default
Real systems mix synchronous REST/gRPC and event-driven Kafka/SQS side by side, choosing per interaction rather than picking a “winner.”
Almost every modern application you interact with — ride-sharing, banking, streaming, ticketing, e-commerce — is quietly running both patterns side by side. Getting the split right is often the difference between a checkout page that feels instant and one that stalls every time an unrelated background service has a bad day.
Why We Need Two Styles — Not One
Why do we even need two different integration styles? Because different business situations have fundamentally different requirements around timing, coupling, and failure tolerance.
2.1 The core tension
Imagine an e-commerce checkout flow. Some parts of it absolutely need an immediate answer: “Is this credit card valid?” cannot be answered “we’ll let you know sometime later.” Other parts of the same flow do not need an immediate answer: “Please send the customer a confirmation email” or “Please update the recommendation engine with this purchase” can happen a few seconds, or even minutes, later without anyone noticing or caring.
If you force everything to be synchronous, you get a system where the checkout page’s response time is the sum of every single downstream operation — payment, inventory, email, analytics, loyalty points, fraud check — all chained together, all blocking. If one of those (say, the email service) is slow or down, the customer cannot check out at all, even though sending the email has nothing to do with whether the payment succeeded.
If you force everything to be asynchronous, you get the opposite problem: the user clicks “Buy Now” and the page immediately says “Order received” without knowing whether the card was even valid, leading to a terrible and confusing user experience.
Think of ordering food at a fast-food counter. “What’s my total?” needs a synchronous answer right now, from the cashier, before you pay. But “please make my burger” is asynchronous — the kitchen works on it in the background, and they call your number when it’s ready. You don’t stand frozen at the counter waiting for the burger to be cooked; you step aside and the system notifies you later.
2.2 Why this matters more in distributed systems
In a single monolithic application, calling another part of the code is essentially free and instantaneous, so this distinction barely matters. But once you split your application into microservices running on different machines, connected over a network, every synchronous call now carries network latency, the possibility of timeouts, and the risk that the other service is temporarily unavailable. This is why the choice of integration style became a first-class architectural decision rather than an implementation detail.
Beginners often think “asynchronous” and “event-driven” are the same thing, or that “synchronous” is simply outdated. Neither is true. Synchronous communication is still the right choice for a huge number of use cases — especially anything user-facing that needs an immediate answer. The goal is not to pick a winner; it’s to use the right tool for each interaction.
The Vocabulary Every Integration Discussion Uses
3.1 What is synchronous integration?
Synchronous integration means Service A sends a request to Service B and then blocks — pauses its own execution — until Service B sends back a response. The calling thread cannot proceed until the answer arrives (or a timeout occurs). The most common real-world implementation is an HTTP request-response call, such as a REST API call or a gRPC unary call.
Key characteristics of synchronous integration
- Request-response pairing: every request expects exactly one matching response.
- Temporal coupling: both the caller and the callee must be running and reachable at the same time for the interaction to succeed.
- Immediate consistency: the caller knows the outcome (success or failure) right away.
- Blocking (usually): the calling thread waits, though non-blocking / async I/O techniques (like reactive programming) can hide this from the OS thread while the logical flow still “waits” for the result before proceeding.
3.2 What is event-driven (asynchronous) integration?
In event-driven integration, a service (the producer or publisher) emits an event — a record describing something that already happened, like OrderPlaced or PaymentCompleted — onto a message broker (also called an event bus or message queue). One or more other services (consumers or subscribers) receive that event, whenever they are ready, and react to it. The producer does not wait for a response and typically does not even know who, if anyone, is listening.
Key characteristics of event-driven integration
- Fire-and-forget (from the producer’s perspective): the producer publishes the event and immediately continues its own work.
- Temporal decoupling: the producer and consumer do not need to be online at the same time; the broker holds the message until a consumer is ready.
- Eventual consistency: the system reaches a consistent state eventually, not instantly.
- Multiple consumers: many independent services can react to the same event without the producer knowing or caring.
“Tell me now.”
“Tell me the answer now, and I’ll wait right here until you do.” A single request, a single response, and the caller stays on the line the whole time.
“This just happened.”
“Here’s something that happened. React to it whenever you’re able to.” The producer publishes a fact and moves on; anyone interested handles it on their own schedule.
3.3 A note on terminology
You will also hear the terms synchronous vs. asynchronous and request-driven vs. event-driven used almost interchangeably. Strictly speaking, “asynchronous” is the broader umbrella (it just means “not blocking”), while “event-driven” is a specific and very popular way of achieving asynchronous communication using a broker and the publish-subscribe model. Another asynchronous-but-not-quite-event-driven pattern is polling, where a client repeatedly asks “is it done yet?” — useful, but generally considered less elegant than true event-driven messaging.
Producer / Publisher — the service that creates and sends an event. Consumer / Subscriber — the service that receives and processes an event. Broker — the middleman infrastructure (Kafka, RabbitMQ, SQS) that stores and routes events. Topic / Queue — a named channel events are published to and consumed from. Payload — the actual data carried inside a message or event.
3.4 Events vs. commands vs. queries
It helps beginners to separate three kinds of messages that flow through a distributed system, because people often conflate them:
- Query — “Tell me something.” Always synchronous by nature, because you need the answer to proceed (e.g., “What is this product’s current price?”).
- Command — “Do something.” Can be synchronous (“Charge this card now, tell me if it worked”) or asynchronous (“Please process this refund when you get a chance”).
- Event — “Something happened.” Always describes the past, is always fire-and-forget from the producer’s side, and is the natural building block of event-driven architecture (e.g.,
RefundProcessed).
A useful naming convention that many teams adopt: commands are named as imperative verbs (ChargeCard, ShipOrder), while events are named in the past tense (CardCharged, OrderShipped). This small convention alone prevents a lot of confusion in large codebases about what a given message actually represents.
3.5 Push vs. pull delivery
Within event-driven systems, there are two common delivery models. In a push model (used by RabbitMQ and SNS), the broker actively delivers messages to consumers as soon as they arrive. In a pull model (used by Kafka), consumers continuously poll the broker for new messages at their own pace. Pull-based systems give consumers more control over their own processing rate, which is part of why Kafka handles very high-throughput workloads well — slow consumers simply fall behind rather than getting overwhelmed by pushed messages they can’t process fast enough.
The Moving Parts on Each Side
4.1 Synchronous architecture components
Client / Caller
The service or application initiating the call and waiting for a response.
Server / Provider service
The service that exposes an endpoint and returns a response.
Protocol
Usually HTTP/1.1, HTTP/2, or gRPC (which itself runs over HTTP/2).
Load balancer
Distributes incoming requests across multiple instances of the provider service.
Timeout & retry logic
Client-side configuration that decides how long to wait and whether to retry on failure.
Circuit breaker
A safety component (like Resilience4j or Hystrix) that stops calling a failing downstream service temporarily to prevent cascading failures.
4.2 Event-driven architecture components
Producer
Publishes events describing state changes onto a named topic or queue.
Message broker / event bus
Middleware such as Apache Kafka, RabbitMQ, Amazon SNS/SQS, or Google Pub/Sub, responsible for durably storing and routing messages.
Topic or queue
A named channel that groups related events (e.g., order-events).
Consumer / consumer group
One or more services that read and process events, often in parallel across partitions.
Schema registry
A component (common in Kafka ecosystems) that enforces the structure of event payloads (Avro, Protobuf) so producers and consumers agree on format.
Dead-letter queue (DLQ)
A holding area for messages that repeatedly fail processing, so they don’t block the rest of the queue.
At Netflix, when you press play on a show, the request to fetch the video manifest is synchronous — you need the video URL right now. But the event that you started watching (used for “continue watching” and recommendations) is published asynchronously to Netflix’s internal event pipeline (built on Kafka) and processed by dozens of downstream systems without you ever noticing any delay.
4.3 How the components fit together in practice
It’s worth walking through what each component is actually responsible for, since beginners often lump them together as “the network stuff.”
On the synchronous side, the load balancer is the first thing a request hits after leaving the client. Its only job is to pick a healthy instance of the target service and forward the request, using an algorithm like round-robin or least-connections, and it continuously removes unhealthy instances from rotation based on health-check endpoints (commonly /actuator/health in Spring Boot). The circuit breaker lives on the client side, wrapping the actual network call, and tracks a rolling window of recent successes and failures; once the failure rate crosses a threshold, it “opens” and short-circuits further calls instantly, without even attempting the network hop, giving the downstream service breathing room to recover.
On the event-driven side, the broker is the most operationally significant piece of infrastructure in the whole architecture, because unlike a load balancer (which is largely stateless), a broker like Kafka is a stateful, disk-backed system that must durably retain data, replicate it, and serve it back out potentially days or weeks later. This is why teams often treat “operate the event backbone reliably” as its own dedicated platform responsibility, separate from any individual service team. The consumer group concept is what allows horizontal scaling on the reading side: Kafka guarantees that within a single consumer group, each partition is read by exactly one consumer instance at a time, so adding more consumer instances (up to the number of partitions) linearly increases how fast a topic’s backlog can be processed.
Step by Step, From First Byte to Final Handler
5.1 How a synchronous call actually works, step by step
- The client opens a TCP connection to the server (or reuses one via HTTP keep-alive / connection pooling).
- The client serialises the request (e.g., to JSON) and sends it over HTTP.
- The client thread blocks, waiting on the socket for a response (or, in reactive frameworks, registers a callback and frees the underlying thread, but the logical operation is still “waiting”).
- The server receives the request, processes it (possibly calling its own database or other services), and constructs a response.
- The server sends the response back over the same connection.
- The client deserialises the response and resumes execution with the result.
- If no response arrives within the configured timeout, the client gives up and treats it as a failure — potentially retrying.
// A synchronous, blocking call from an Order Service to an Inventory Service
@Service
public class InventoryClient {
private final RestClient restClient;
public InventoryClient(RestClient.Builder builder) {
this.restClient = builder.baseUrl("http://inventory-service").build();
}
public StockResponse checkStock(String sku, int quantity) {
// This call blocks the current thread until Inventory Service responds
// or the configured timeout is reached.
return restClient.get()
.uri("/inventory/{sku}?qty={qty}", sku, quantity)
.retrieve()
.body(StockResponse.class);
}
}
5.2 How event-driven communication actually works, step by step
- The producer service constructs an event object (e.g.,
OrderPlacedEvent) describing something that has already happened. - The producer serialises the event (commonly JSON, Avro, or Protobuf) and sends it to the broker, specifying a topic / queue name.
- The broker durably persists the event, typically writing it to disk and replicating it across multiple broker nodes for fault tolerance.
- The producer’s call to the broker returns almost immediately (an acknowledgment that the message was accepted) — it does not wait for any consumer to process it.
- One or more consumers, running independently and possibly at different times, poll or subscribe to the topic and receive the event.
- Each consumer processes the event according to its own logic and, on success, commits an offset (in Kafka) or acknowledges the message (in RabbitMQ / SQS) so it won’t be redelivered.
- If a consumer fails to process the event, the broker can redeliver it (based on retry policy) or route it to a dead-letter queue after repeated failures.
// Producer: publishes an event and does NOT wait for it to be processed
@Service
public class OrderEventPublisher {
private final KafkaTemplate<String, OrderPlacedEvent> kafkaTemplate;
public OrderEventPublisher(KafkaTemplate<String, OrderPlacedEvent> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void publishOrderPlaced(Order order) {
OrderPlacedEvent event = new OrderPlacedEvent(
order.getId(), order.getCustomerId(), order.getTotalAmount());
// Fire-and-forget: returns a Future immediately, does not block
// the checkout flow waiting for consumers to react.
kafkaTemplate.send("order-events", order.getId(), event);
}
}
// Consumer: reacts to the event whenever it is ready to
@Component
public class EmailNotificationConsumer {
@KafkaListener(topics = "order-events", groupId = "email-service")
public void onOrderPlaced(OrderPlacedEvent event) {
// Runs independently, seconds or minutes after the order was placed.
// A failure here never affects the checkout response the customer saw.
emailService.sendOrderConfirmation(event.customerId(), event.orderId());
}
}
In the synchronous example, checkStock() returns a value that the caller directly uses to decide the next step. In the event-driven example, publishOrderPlaced() returns almost instantly and has no idea whether the email was ever sent, or when.
A Realistic Checkout, End to End
Let’s trace a realistic e-commerce checkout across its full lifecycle, mixing both styles the way a real system would.
Notice the deliberate split: the two steps that the user is directly waiting on — placing the order and charging the card — are synchronous, because the user needs to know right now whether checkout succeeded. Everything after the response is sent back (sending an email, updating recommendations) is handled asynchronously through the event, because the user does not need to wait for those side effects, and a slow email provider should never be able to make checkout feel slow.
6.1 The lifecycle of a synchronous request
- Connection establishment — TCP handshake (and TLS handshake for HTTPS), often skipped if a pooled connection is reused.
- Request send — headers and body transmitted.
- Processing — server does its work, possibly calling further synchronous downstream services.
- Response send — status code, headers, and body returned.
- Connection teardown or reuse — connection closed or returned to the pool.
6.2 The lifecycle of an event
- Creation — the producer builds the event object after a state change has already committed (e.g., after the order row is saved to the database).
- Publish — the event is sent to the broker and durably stored, often replicated across 3 broker nodes for safety.
- Retention — the broker retains the event for a configured period (Kafka can retain events for days or indefinitely) even after consumers have read it, allowing replay.
- Delivery — each interested consumer group receives its own copy of the event independently.
- Processing — the consumer executes its business logic.
- Acknowledgment / offset commit — the consumer confirms successful processing so the broker knows not to redeliver.
- Failure handling — on error, the message is retried, delayed, or moved to a dead-letter queue.
A very common beginner mistake is writing to the database and publishing the event as two separate, unrelated steps. If the service crashes between the database commit and the event publish, the event is lost forever and downstream systems never find out. The industry-standard fix is the Transactional Outbox Pattern, covered later in the Design Patterns section.
The Honest Comparison
| Dimension | Synchronous Integration | Event-Driven Integration |
|---|---|---|
| Immediate answer | Yes — caller gets the result right away | No — caller only knows the event was accepted, not the outcome |
| Coupling | Tighter — caller and callee must both be up and reachable | Looser — producer and consumer can be online at different times |
| Failure impact | A downstream outage can directly fail or slow the caller | Broker absorbs failures; consumer catches up later |
| Consistency model | Strong / immediate consistency | Eventual consistency |
| Mental model | Simple, linear, easy to trace step by step | Harder to trace; effects are distributed across many consumers over time |
| Scalability under load | Limited by the slowest synchronous dependency in the chain | Broker naturally buffers spikes; consumers scale independently |
| Best suited for | User-facing reads / writes needing an instant answer (login, payment authorisation, price lookup) | Background work, cross-service notifications, analytics, workflows that can tolerate delay |
| Debugging | Easier — a single request / response pair, visible in one trace | Harder — requires distributed tracing across producer and many consumers |
| Infrastructure needed | Just HTTP / gRPC clients and servers | A message broker cluster (Kafka, RabbitMQ, SQS/SNS) that must itself be operated and scaled |
When to choose synchronous
- The caller genuinely cannot proceed without the answer (e.g., “Is this password correct?”).
- The operation is a simple read that must reflect the absolute latest data.
- Low latency and simplicity matter more than resilience to downstream outages.
When to choose event-driven
- The caller doesn’t need to know the outcome immediately, only that the request was accepted.
- Multiple, possibly unknown-in-advance, services need to react to the same fact.
- You want to protect the caller from downstream slowness or outages (decoupling failure domains).
- You need to smooth out traffic spikes (the broker acts as a buffer).
Ask: “Does the user or calling service need to know the result before it can safely continue?” If yes → synchronous. If the result can be found out later, or doesn’t need to be known by the caller at all → event-driven.
Latency Stacking, Reactive I/O, and Load Levelling
8.1 Synchronous performance characteristics
In a synchronous chain, total latency is roughly the sum of every hop’s latency, because each call waits for the previous one. If Service A calls B, which calls C, which calls D, and each takes 100 ms, the user waits at least 300 ms — plus network overhead at each hop. This is called latency stacking, and it is one of the biggest performance risks in microservice architectures with deep synchronous call chains.
Scaling a synchronous system under heavy load usually means horizontally scaling the server (adding more instances behind a load balancer) and tuning thread pools and connection pools so the client doesn’t run out of capacity to make new requests. Techniques like connection pooling, HTTP/2 multiplexing, and reactive / non-blocking I/O (e.g., Spring WebFlux, Project Reactor) help a single machine handle far more concurrent synchronous requests without needing a thread per request.
@Service
public class ReactiveInventoryClient {
private final WebClient webClient;
public ReactiveInventoryClient(WebClient.Builder builder) {
this.webClient = builder.baseUrl("http://inventory-service").build();
}
// Logically still "request/response" — the caller needs the answer —
// but the underlying thread is freed while waiting, improving throughput.
public Mono<StockResponse> checkStock(String sku) {
return webClient.get()
.uri("/inventory/{sku}", sku)
.retrieve()
.bodyToMono(StockResponse.class)
.timeout(Duration.ofSeconds(2));
}
}
8.2 Event-driven performance characteristics
Event-driven systems decouple the producer’s response time from the total processing time. The checkout page responds as soon as the event is accepted by the broker (usually a few milliseconds), regardless of how long downstream consumers take. This means the perceived performance for the user is excellent even if background processing takes seconds.
Under heavy load, brokers like Kafka can absorb huge bursts of events because writes are largely sequential disk appends, which are extremely fast. Consumers can then process the backlog at their own sustainable pace — this is called load levelling or buffering. Scaling consumers is done by adding more consumer instances within a consumer group; Kafka automatically distributes topic partitions across them.
Uber’s trip-matching and pricing pipeline uses synchronous calls for the split-second decision of “which driver gets this ride” (needs an instant answer), while the vast stream of location pings, trip-completion events, and surge-pricing recalculations flow through Kafka-based event pipelines that can absorb millions of events per second without ever slowing down the rider’s app.
If consumers process events slower than producers create them, a backlog called consumer lag builds up. Left unchecked, “eventually consistent” can silently become “very delayed,” which surprises teams who assumed eventual consistency meant seconds, not hours. Always monitor consumer lag as a first-class metric.
Timeouts, Circuit Breakers, Idempotency, and CAP
9.1 Failure handling in synchronous systems
Timeouts
Never wait forever — always bound how long you’ll wait for a response.
Retries with backoff
Retry transient failures, but with exponential backoff and jitter to avoid overwhelming a recovering service.
Circuit breakers
Stop calling a service that is repeatedly failing, and fail fast instead, giving it room to recover.
Bulkheads
Isolate thread pools per downstream dependency so one slow service can’t exhaust resources needed for calls to other services.
Graceful degradation
Return cached or default data when the live call fails, rather than failing the whole user request.
@Service
public class ResilientInventoryClient {
private final RestClient restClient;
private final CircuitBreaker circuitBreaker;
public ResilientInventoryClient(RestClient.Builder builder,
CircuitBreakerRegistry registry) {
this.restClient = builder.baseUrl("http://inventory-service").build();
this.circuitBreaker = registry.circuitBreaker("inventoryService");
}
public StockResponse checkStock(String sku) {
Supplier<StockResponse> call = () -> restClient.get()
.uri("/inventory/{sku}", sku)
.retrieve()
.body(StockResponse.class);
return circuitBreaker.executeSupplier(call);
}
}
9.2 Failure handling in event-driven systems
Broker replication
Kafka replicates each partition across multiple brokers (controlled by replication.factor), so a single broker failure doesn’t lose data.
At-least-once delivery
Most brokers guarantee a message will be delivered at least once, which means consumers must be idempotent — processing the same event twice must not cause duplicate side effects (e.g., charging a card twice).
Dead-letter queues (DLQ)
After N failed processing attempts, move the message aside so it doesn’t block the rest of the queue, and alert engineers.
Restart & offset tracking
If a consumer crashes, it resumes from its last committed offset rather than losing track of progress.
An idempotent consumer keeps a table of already-processed event IDs. Before processing an incoming event, it checks whether that ID has been seen before; if so, it skips processing but still acknowledges the message. This protects against duplicate delivery, a normal and expected part of most broker guarantees.
9.3 CAP theorem context
The CAP theorem states that a distributed system can only guarantee two of three properties at any moment during a network partition: Consistency (every read sees the latest write), Availability (every request gets a response), and Partition tolerance (the system keeps working despite network failures between nodes). Synchronous integration tends to favour consistency — if the downstream service can’t confirm the latest state, the caller often prefers to fail rather than proceed with stale data. Event-driven integration tends to favour availability — the producer keeps accepting and publishing events even if some consumers are temporarily unreachable, accepting that consumers will catch up later (eventual consistency).
TLS, mTLS, ACLs, and the Cost of Fan-out
10.1 Securing synchronous integrations
- Transport security: always use TLS / HTTPS to encrypt data in transit between services.
- Authentication: API keys, OAuth2 / OIDC access tokens, or mutual TLS (mTLS) between services in a service mesh.
- Authorisation: scopes / roles checked on every endpoint to ensure the caller is allowed to perform the action.
- Rate limiting: protects the server from being overwhelmed by too many synchronous requests, whether malicious or accidental.
- Input validation: since the caller gets an immediate response, validation errors can be surfaced directly and clearly.
10.2 Securing event-driven integrations
- Broker authentication: producers and consumers authenticate to the broker itself (e.g., SASL/SCRAM or mTLS for Kafka).
- Topic-level authorisation (ACLs): restrict which services can publish to or consume from a given topic, since a compromised consumer credential could otherwise read sensitive event streams.
- Payload encryption: sensitive fields inside event payloads may need field-level encryption, since events are often retained for days and copied to multiple consumers.
- Schema validation: a schema registry prevents malformed or malicious payloads from being accepted onto a topic.
- Audit trail: because brokers retain history, event streams can double as a natural audit log of “what happened, when” — valuable for security investigations.
Because event-driven systems fan out data to many consumers automatically, it’s easy to accidentally over-expose sensitive information (like full customer PII) to services that only needed a small subset of the data. Apply the principle of least privilege to event payload design just as strictly as to API design.
Observability Across Both Styles
11.1 Observing synchronous systems
- Latency metrics: p50 / p95 / p99 response times per endpoint.
- Error rates: HTTP status codes (4xx client errors, 5xx server errors) tracked per route.
- Distributed tracing: tools like OpenTelemetry, Zipkin, or Jaeger propagate a correlation ID (trace ID) across every hop of a synchronous chain, letting you see the full waterfall of a request across services.
11.2 Observing event-driven systems
- Consumer lag: how far behind is each consumer group from the latest published event? This is arguably the single most important event-driven metric.
- Throughput: messages published / consumed per second, per topic.
- DLQ size: a growing dead-letter queue is an early warning sign of a systemic processing bug.
- End-to-end correlation: the event payload should carry a correlation ID (often propagated from the originating HTTP request) so you can trace a business transaction across both synchronous calls and asynchronous events in the same trace view.
public record OrderPlacedEvent(
String orderId,
String customerId,
BigDecimal totalAmount,
String correlationId
) {}
@RestController
public class CheckoutController {
@PostMapping("/checkout")
public OrderResponse checkout(@RequestBody CheckoutRequest req,
@RequestHeader("X-Correlation-Id") String correlationId) {
Order order = orderService.placeOrder(req);
eventPublisher.publishOrderPlaced(order, correlationId);
return new OrderResponse(order.getId(), "CONFIRMED");
}
}
Always log both the correlation ID and a stable business key (like order ID) at every hop — synchronous or asynchronous — so support engineers can reconstruct “what happened to order #12345” across the entire system, weeks after the fact.
Rolling Deploys, Managed Brokers, and Cell-Based Architectures
12.1 Deploying synchronous services
Synchronous services are typically deployed as stateless containers behind a load balancer, orchestrated by Kubernetes (using a Deployment and a Service / Ingress) or a managed platform like AWS ECS / Fargate. Horizontal Pod Autoscaling reacts to CPU or request-latency metrics to add or remove instances as traffic changes. Because clients are waiting live, rollout strategies like rolling updates or blue-green deployments matter a great deal — a bad deploy shows up immediately as failed requests.
12.2 Deploying event-driven infrastructure
Run your own broker cluster
Apache Kafka or RabbitMQ on Kubernetes (via operators) or VMs, requiring careful capacity planning for disk, replication, and partition counts.
Let the cloud run it
Amazon MSK / SQS / SNS, Google Cloud Pub/Sub, Azure Event Hubs, or Confluent Cloud — offloading operational burden (patching, scaling, replication) to the cloud provider.
Because consumers can lag behind or fail independently of producers, deployments of consumer services are generally lower-risk than synchronous services — a bad consumer deploy causes processing delay, not a user-facing outage, as long as the DLQ and alerting are in place.
Amazon’s order pipeline famously uses SQS queues extensively between its internal services specifically so that a slow or failed component (say, gift-wrapping logic) never blocks the core checkout path. This “cell-based,” queue-heavy architecture is part of why Amazon’s site stays responsive even when individual backend components are degraded.
Where the Data Actually Lives
13.1 Synchronous integration and data
Synchronous services typically read and write directly to their own database and rely on caching (Redis, Memcached, or in-memory caches) to reduce the latency of repeated synchronous reads. A load balancer (round-robin, least-connections, or consistent hashing) sits in front of a pool of service instances, spreading synchronous requests evenly and routing around unhealthy instances via health checks.
13.2 Event-driven integration and data · Event Sourcing & CQRS
Event-driven thinking extends naturally into how data itself is stored. In Event Sourcing, instead of storing only the current state of an entity, the system stores the full sequence of events that led to that state (e.g., AccountOpened, MoneyDeposited, MoneyWithdrawn), and the current balance is derived by replaying those events. This pairs naturally with CQRS (Command Query Responsibility Segregation), where writes go through a command model that emits events, and reads are served from a separate, denormalised read model kept up to date by consuming those same events — often via materialised views optimised for fast queries.
Database replication (keeping copies of data on multiple nodes) and partitioning / sharding (splitting data across nodes by key) apply to both styles, but event-driven systems additionally rely on the broker’s own partitioning: a Kafka topic is split into partitions, and events with the same key (e.g., the same order ID) always land on the same partition, preserving per-key ordering even while the topic scales across many machines.
A synchronous database read is like calling the bank and asking your exact balance right now. Event sourcing is like keeping your full bank statement (every transaction ever made) and calculating your balance by adding them all up — slower to compute from scratch, but it never loses the “why,” and you can always answer “what was my balance last Tuesday?”
In practice, few teams adopt full event sourcing for every entity in their system, since replaying long event histories on every read can be slow without additional optimisation. A common middle ground is to keep a normal, current-state table for fast reads, while still publishing every state change as an event to the broker for other services to consume — getting many of the benefits of an event-driven audit trail without paying the full cost of rebuilding state from history on every query. Snapshotting (periodically saving the computed current state alongside the event log) is the standard technique for keeping full event sourcing fast even as history grows very long.
REST for Questions, Events for Facts
In a microservices architecture, services need two different kinds of connective tissue: APIs for synchronous, direct questions, and events for broadcasting facts. Most real systems use both together, structured roughly like this:
REST / gRPC APIs
For anything a client or another service needs answered immediately — fetching a product page, validating a coupon code, checking a user’s permissions.
API Gateway
The single synchronous entry point for external clients, handling routing, authentication, and rate limiting before forwarding to internal services.
Service discovery
So synchronous clients can find healthy instances of the service they need to call, without hardcoding IP addresses.
Event backbone
Kafka / RabbitMQ for everything that represents “a fact happened” and needs to reach several independent services — inventory updates, audit logging, analytics, notification fan-out.
14.1 Bounded contexts and integration style
A useful architectural rule: within a bounded context (a tightly related group of capabilities owned by one team), synchronous calls are often fine because the services are closely related and usually deployed together. Across bounded contexts (e.g., Orders talking to Shipping, a completely different domain owned by a different team), event-driven integration is usually preferred, because it avoids creating a hard dependency between teams that release on different schedules.
Google’s internal service mesh relies heavily on synchronous RPC (using their own protocol, a precursor to what became gRPC) for the vast majority of request-serving traffic like Search, because users expect an answer within milliseconds. Meanwhile, background indexing pipelines that crawl and process the web operate as massive asynchronous, event/batch-driven pipelines that can take minutes or hours — a clear illustration of matching the pattern to the latency requirement.
Patterns Worth Copying and Anti-Patterns Worth Fearing
15.1 Useful patterns
Request-Reply over messaging
A hybrid pattern where a request is sent asynchronously through a queue but includes a “reply-to” address, letting the caller still eventually correlate a response — useful for decoupling transport while keeping a logical request/response relationship.
Transactional Outbox
Instead of writing to the database and publishing an event as two separate operations (risking the dual-write problem), the service writes the event into an “outbox” table in the same database transaction as the business data. A separate process (like Debezium, using Change Data Capture) then reliably publishes rows from the outbox to the broker.
Saga Pattern
Coordinates a multi-step business transaction across several services using a sequence of events, with explicit compensating events to undo previous steps if a later step fails — since distributed systems can’t use a single ACID transaction across services.
Circuit Breaker
Covered earlier — protects synchronous callers from cascading failures.
Competing Consumers
Multiple instances of the same consumer read from the same queue/partition to share the processing load and increase throughput.
A closer look at the Saga Pattern
Imagine booking a trip that requires reserving a flight, a hotel, and a rental car across three independent services. In a single-database world, you’d wrap all three in one ACID transaction and roll back everything on any failure. Across three separate microservices, that’s not possible — there is no single transaction spanning all of them. A Saga solves this by breaking the operation into a sequence of local transactions, each publishing an event that triggers the next step:
FlightReservedevent published after the flight service succeeds.- Hotel service consumes it, reserves the room, publishes
HotelReserved. - Car rental service consumes it, reserves the car, publishes
CarReserved, completing the saga.
If the car rental step fails, the saga publishes compensating events (HotelReservationCancelled, FlightReservationCancelled) that walk backward through the already-completed steps, undoing them one by one. This is why sagas are sometimes described as trading strong ACID guarantees for a carefully designed, event-driven “undo” chain.
Transactional Outbox in one picture
15.2 Anti-patterns to avoid
The Distributed Monolith
Chaining many synchronous calls across services so tightly that you can’t deploy or scale one without affecting all the others — you get all the operational cost of microservices with none of the independence benefits.
Chatty synchronous chains
Deeply nested synchronous calls (A → B → C → D → E) that stack latency and multiply the chance of failure at every hop.
The dual-write problem
Writing to the database and publishing an event as two unrelated operations, risking silently lost events (fixed by the Transactional Outbox pattern above).
Event soup
Publishing dozens of loosely defined, undocumented events with no schema governance, making it impossible to know who depends on what — solved with a schema registry and clear event ownership.
Events where you needed a direct answer
Forcing a genuinely synchronous need (like “was my payment approved?”) through asynchronous messaging just because “event-driven is more scalable,” resulting in a confusing, polling-heavy user experience.
The Habits That Keep Integrations Healthy
16.1 Best practices
- Choose the integration style per interaction, not per service — a single service will usually expose both synchronous endpoints and publish events.
- Always set timeouts on every synchronous call; never rely on defaults.
- Make every event consumer idempotent, since at-least-once delivery is the norm, not the exception.
- Use the Transactional Outbox pattern (or a broker with built-in transactional writes, like Kafka transactions) to avoid the dual-write problem.
- Version your event schemas explicitly and use a schema registry so producers can evolve payloads without breaking existing consumers.
- Propagate a correlation ID through both synchronous calls and asynchronous events for unified tracing.
- Monitor consumer lag and DLQ size as first-class production metrics, just like you’d monitor synchronous error rates.
- Keep event payloads focused — publish what happened, not an entire denormalised dump of your database, to limit blast radius and coupling.
16.2 Common mistakes beginners make
| Mistake | Why it hurts | Fix |
|---|---|---|
| Treating “eventual consistency” as “the user won’t notice” | Without a realistic upper bound, backlogs can silently grow into hours | Set an SLO on end-to-end delay and alert when it’s breached |
| Non-idempotent event handlers | Duplicate delivery causes double charges, duplicate emails, etc. | Track processed event IDs and skip duplicates before applying side effects |
| Deep synchronous chains | A single slow dependency degrades the whole system via latency stacking | Break the chain with async events or move slow work off the request path |
| Skipping the DLQ | A single malformed event blocks an entire partition or queue indefinitely | Configure a DLQ per topic with alerting on non-zero size |
| Ignoring schema evolution | Breaking changes silently crash consumers in production | Use a schema registry with explicit backward/forward compatibility rules |
“Event-driven is always more scalable, so we should use it everywhere.” In reality, event-driven architecture introduces real operational complexity (broker management, harder debugging, eventual consistency to reason about). Reach for it deliberately, for interactions that genuinely benefit from decoupling — not as a default for every service-to-service call.
16.3 A quick decision checklist
When designing a new integration point between two services, walking through these questions in order tends to lead to the right choice quickly:
- Does the caller need to know the outcome before it can safely respond to its own caller (e.g., the end user)? If yes, lean synchronous.
- Could more than one service plausibly need to react to this same fact, now or in the future? If yes, lean event-driven, since a single event can fan out to any number of consumers without the producer changing at all.
- Is the operation naturally a background task — something that doesn’t block the primary user journey? If yes, lean event-driven.
- Would a temporary outage of the downstream service be acceptable to “queue up and process later”? If yes, event-driven adds real resilience. If not (e.g., a fraud check that must happen before money moves), synchronous with strong resilience patterns is usually safer.
- Is this interaction inside a single, tightly-owned bounded context, or crossing into a different team’s domain? Crossing boundaries often favours the looser coupling of events.
Most experienced architects don’t treat this as a one-time decision either — it’s common to start a feature synchronously because it’s simpler to build and reason about, and later refactor a specific interaction to be event-driven once real scale or reliability requirements make the trade-off worthwhile. Starting simple and evolving deliberately, backed by real production metrics, is generally a better strategy than trying to predict every scaling requirement upfront.
How the Big Names Split the Two Styles
| Company | Synchronous Use | Event-Driven Use |
|---|---|---|
| Netflix | Fetching video manifests and license URLs before playback starts | Viewing activity, recommendations, and A/B test data flow through Kafka-based pipelines |
| Amazon | Checking product availability and price at the moment of “Buy Now” | Order fulfilment stages (packing, shipping, gift-wrap) coordinated via SQS / SNS queues |
| Uber | Matching a rider to a nearby driver and confirming the fare estimate | Location pings, trip-completion, and surge-pricing recalculation via Kafka streams |
| Search query serving, requiring millisecond RPC responses | Web crawling and indexing pipelines processed as large-scale asynchronous batch/event jobs |
Netflix
Playback path is synchronous for instant response; every downstream analytics, recommendation, and billing signal is an event on the internal Kafka backbone.
Amazon
Buy-Now is synchronous; fulfilment, gift-wrap, warehouse routing, and shipping notifications are queued via SQS / SNS so a slow downstream never blocks checkout.
Uber
Driver-matching and fare confirmation are synchronous; location pings and surge-pricing recalculation ride on Kafka streams at millions of events per second.
Query-serving RPCs must return in milliseconds; the multi-day web crawl and indexing pipeline is a giant asynchronous batch/event system running in the background.
Across all four companies, the pattern is consistent: whatever the end user is staring at the screen waiting for is synchronous; whatever can happen in the background — notifications, analytics, recommendations, fulfilment coordination — is event-driven. This split isn’t accidental; it’s the direct consequence of matching integration style to actual latency requirements, exactly as discussed throughout this guide.
The Portable Answers, in One Place
Frequently asked questions
Q1 · Is REST always synchronous and messaging always asynchronous?
Not strictly. REST is most commonly used synchronously, but you can build asynchronous REST flows (e.g., returning a 202 Accepted and a status URL to poll). Similarly, messaging can be used in a request-reply style that mimics synchronous behaviour. The labels describe the most common usage, not an absolute rule.
Q2 · Can a system use both styles for the same feature?
Yes, and most production systems do exactly this — a synchronous call handles the immediate user-facing decision, and an event is published afterward to trigger background side effects, as shown in the checkout example throughout this guide.
Q3 · Does event-driven mean I lose data consistency?
You trade immediate (strong) consistency for eventual consistency. Data does become consistent, just not instantly. Whether that delay is acceptable depends entirely on the business use case.
Q4 · Which style is easier for a beginner to start with?
Synchronous REST APIs are generally easier to learn first, since the request/response model matches how most programming already works. Event-driven architecture introduces additional concepts (brokers, offsets, idempotency, eventual consistency) that are worth learning once you’re comfortable with synchronous design.
Q5 · What happens if the message broker itself goes down?
Well-run brokers are deployed as clusters (e.g., a 3-node Kafka cluster) specifically so that no single node’s failure takes down the whole broker. Producers typically buffer briefly and retry, and once the broker recovers, message flow resumes without data loss, assuming proper replication was configured.
Q6 · How do I test event-driven flows if there’s no immediate response to assert on?
Rather than asserting on a direct return value, tests typically publish an event and then poll (with a short timeout) for the expected side effect — a database row appearing, a downstream event being published, or a message landing on a test consumer. Tools like Testcontainers can spin up a real Kafka or RabbitMQ broker for integration tests, giving much higher confidence than mocking the broker entirely.
Q7 · Should I use gRPC or REST for synchronous service-to-service calls?
REST (JSON over HTTP) remains the most common choice for public-facing APIs because it is human-readable and universally supported. gRPC, which uses Protocol Buffers over HTTP/2, is often preferred for internal, high-throughput service-to-service calls because of its smaller payload size, strongly typed contracts, and built-in support for streaming. Many organisations use REST at the edge and gRPC internally.
Summary
If you take away one idea from this entire guide, let it be this: synchronous and event-driven integration are not rival philosophies competing for the title of “correct architecture.” They are two complementary tools, each suited to a different shape of problem — one built for the moment a caller genuinely cannot move forward without an answer, and the other built for the moment a fact needs to ripple outward to however many interested parties exist, now or in the future, without anyone having to wait around for it. Almost every mature, high-scale system you interact with daily — from checking out on Amazon to hailing a ride on Uber — is quietly running both patterns side by side, each doing the job it was designed for.
Key takeaways
- Synchronous integration is a request-response call where the caller blocks and waits for an immediate answer — ideal when the caller genuinely cannot proceed without knowing the result.
- Event-driven integration lets a producer announce that something happened and move on, while independent consumers react whenever they’re ready — ideal for decoupled, background, or fan-out work.
- The core trade-off is immediate consistency and simplicity (synchronous) versus loose coupling and resilience to downstream failure (event-driven).
- Real production systems use both, choosing per interaction based on whether the caller needs to know the outcome right now.
- Event-driven systems require extra discipline: idempotent consumers, the Transactional Outbox pattern, schema governance, and vigilant monitoring of consumer lag and dead-letter queues.
- Synchronous systems require their own discipline: timeouts, retries with backoff, circuit breakers, and bulkheads to prevent cascading failure.
- The CAP theorem helps explain the underlying tension: synchronous designs typically lean toward consistency, event-driven designs typically lean toward availability.
Synchronous integration is for the moments a caller cannot move forward without an answer; event-driven integration is for the moments a fact needs to ripple outward to many interested parties on their own schedule — and every serious production system runs them side by side, one interaction at a time.