Synchronous vs Event-Driven Integration

Synchronous vs Event-Driven Integration

A beginner-to-production walkthrough of how services talk to each other — why some calls wait for an answer while others fire and move on, and how to choose the right style for your architecture.

01 · INTRODUCTION & HISTORY

Two Ways For Software To Talk To Software

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.

Real-life analogy

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 is 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 do not sit around waiting.

How the two styles matured side by side

It is 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.

1970s

Mainframe batch queues

Departmental data moves overnight through message queues — the ancestor of modern event-driven systems.

1990s

RPC & SOAP

Synchronous “call another service like it’s a local function” becomes the enterprise default.

2000s

SOA & JMS / IBM MQ

Enterprise messaging is standardised long before the word “microservices” is common.

2011

Apache Kafka at LinkedIn

A durable, replayable, partitioned event log becomes the new backbone of event-driven architecture.

2015+

gRPC & hybrid systems

Google’s high-performance RPC framework arrives; modern systems freely mix REST, gRPC, and event streaming.

02 · THE PROBLEM & MOTIVATION

Different Interactions, Different Needs

Why do we even need two different integration styles? Because different business situations have fundamentally different requirements around timing, coupling, and failure tolerance.

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.

Beginner example

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.

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.

Common misconception

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.

“Force everything into one style and you build a fragile system. Match each interaction to its natural style and you build a resilient one.”

03 · CORE CONCEPTS

The Vocabulary You Will See Everywhere

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.

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.

Synchronous, in one sentence

  • “Tell me the answer now, and I’ll wait right here until you do.”

Event-driven, in one sentence

  • “Here’s something that happened. React to it whenever you’re able to.”

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.

Terminology cheat sheet

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.

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.

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 cannot process fast enough.

04 · ARCHITECTURE & COMPONENTS

What Each Style Is Actually Made Of

Synchronous architecture components

CL

Client

The service or application initiating the call.

SV

Server / Provider service

The service that exposes an endpoint and returns a response.

TP

Transport protocol

Usually HTTP/1.1, HTTP/2, or gRPC (which runs over HTTP/2).

LB

Load balancer

Distributes incoming requests across multiple instances of the provider service.

TO

Timeout & retry logic

Client-side configuration that decides how long to wait and whether to retry on failure.

CB

Circuit breaker

A safety component (like Resilience4j or Hystrix) that stops calling a failing downstream service temporarily to prevent cascading failures.

SYNCHRONOUS INTEGRATION Client sends a request and blocks — execution pauses until the server replies (or the timeout fires). Client Service calling thread waits Server Service REST / gRPC endpoint 1 · HTTP Request 2 · HTTP Response
Fig 1 · Synchronous request/response — a single connected conversation between exactly two parties.

Event-driven architecture components

PR

Producer

Publishes events describing state changes.

BR

Message broker / event bus

Middleware such as Apache Kafka, RabbitMQ, Amazon SNS/SQS, or Google Pub/Sub, responsible for durably storing and routing messages.

TQ

Topic or queue

A named channel that groups related events (e.g., order-events).

CG

Consumer / consumer group

One or more services that read and process events, often in parallel across partitions.

SR

Schema registry

A component (common in Kafka ecosystems) that enforces the structure of event payloads (e.g., using Avro or Protobuf) so producers and consumers agree on format.

DL

Dead-letter queue (DLQ)

A holding area for messages that repeatedly fail processing, so they don’t block the rest of the queue.

EVENT-DRIVEN INTEGRATION Producer emits a fact; the broker fans it out to any number of independent consumers, each at their own pace. Producer Service fire-and-forget Message Broker / Topic Kafka · RabbitMQ · SNS/SQS durable · replicated · replayable Consumer A Consumer B Consumer C 1 · Publish Event 2 · Deliver 2 · Deliver 2 · Deliver
Fig 2 · Event-driven fan-out — one publish, N independent reactions, no producer-side wait.
Production example

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.

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.

05 · INTERNAL WORKING

Step By Step, Under The Hood

How a synchronous call actually works, step by step

1

Open connection

The client opens a TCP connection to the server (or reuses one via HTTP keep-alive / connection pooling).

2

Serialize & send

The client serialises the request (e.g., to JSON) and sends it over HTTP.

3

Block on socket

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.”

4

Server processes

The server receives the request, processes it (possibly calling its own database or other services), and constructs a response.

5

Return response

The server sends the response back over the same connection.

6

Client resumes

The client deserialises the response and resumes execution with the result.

7

Timeout path

If no response arrives within the configured timeout, the client gives up and treats it as a failure — potentially retrying.

JAVA · SYNCHRONOUS REST CALL USING SPRING’S RESTCLIENT
// 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);
    }
}

How event-driven communication actually works, step by step

1

Build event

The producer service constructs an event object (e.g., OrderPlacedEvent) describing something that has already happened.

2

Serialize & send

The producer serialises the event (commonly JSON, Avro, or Protobuf) and sends it to the broker, specifying a topic/queue name.

3

Broker persists

The broker durably persists the event, typically writing it to disk and replicating it across multiple broker nodes for fault tolerance.

4

Producer moves on

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.

5

Consumers receive

One or more consumers, running independently and possibly at different times, poll or subscribe to the topic and receive the event.

6

Ack / commit offset

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.

7

Retry or DLQ

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.

JAVA · EVENT-DRIVEN PRODUCER AND CONSUMER USING SPRING KAFKA
// 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());
    }
}
Notice the key difference

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.

06 · DATA FLOW & LIFECYCLE

One Real Checkout, Traced End-to-End

Let’s trace a realistic e-commerce checkout across its full lifecycle, mixing both styles the way a real system would.

CHECKOUT LIFECYCLE (MIXED) User-facing steps run synchronously; after the response, an event fans out to background side effects. User Order Service Payment Service Message Broker Email Service Recommendation Place Order (HTTP · sync) Charge Card (HTTP · sync) Payment Approved Order Confirmed (HTTP response) Publish OrderPlacedEvent (async) Deliver event Deliver event Send confirmation email Update recommendations
Fig 3 · The e-commerce checkout — user-blocking steps stay synchronous; everything after the response fans out over events.

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.

The lifecycle of a synchronous request

  1. Connection establishment — TCP handshake (and TLS handshake for HTTPS), often skipped if a pooled connection is reused.
  2. Request send — headers and body transmitted.
  3. Processing — server does its work, possibly calling further synchronous downstream services.
  4. Response send — status code, headers, and body returned.
  5. Connection teardown or reuse — connection closed or returned to the pool.

The lifecycle of an event

  1. 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).
  2. Publish — the event is sent to the broker and durably stored, often replicated across 3 broker nodes for safety.
  3. 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.
  4. Delivery — each interested consumer group receives its own copy of the event independently.
  5. Processing — the consumer executes its business logic.
  6. Acknowledgment / offset commit — the consumer confirms successful processing so the broker knows not to redeliver.
  7. Failure handling — on error, the message is retried, delayed, or moved to a dead-letter queue.
A subtle but critical detail: the “dual write” problem

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.

07 · PROS, CONS & TRADEOFFS

Nine Dimensions, Two Styles

DimensionSynchronousEvent-Driven
Immediate answerYes — caller gets the result right awayNo — caller only knows the event was accepted, not the outcome
CouplingTighter — caller and callee must both be up and reachableLooser — producer and consumer can be online at different times
Failure impactA downstream outage can directly fail or slow the callerBroker absorbs failures; consumer catches up later
Consistency modelStrong / immediate consistencyEventual consistency
Mental modelSimple, linear, easy to trace step by stepHarder to trace; effects are distributed across many consumers over time
Scalability under loadLimited by the slowest synchronous dependency in the chainBroker naturally buffers spikes; consumers scale independently
Best suited forUser-facing reads/writes needing an instant answer (login, payment authorisation, price lookup)Background work, cross-service notifications, analytics, workflows that can tolerate delay
DebuggingEasier — a single request/response pair, visible in one traceHarder — requires distributed tracing across producer and many consumers
Infrastructure neededJust HTTP/gRPC clients and serversA 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).
Rule of thumb

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.

08 · PERFORMANCE & SCALABILITY

Latency Stacks Vs. The Broker Buffer

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 100ms, the user waits at least 300ms — 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.

JAVA · NON-BLOCKING (REACTIVE) SYNCHRONOUS-STYLE CALL, STILL REQUEST/RESPONSE
@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));
    }
}

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.

N × hopslatency stacks in deep sync chains
~mstypical broker accept latency
Millions/sevents a Kafka cluster can absorb
= partitionsmax useful consumer parallelism per group
Production example

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.

Watch out for: consumer lag

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.

09 · HIGH AVAILABILITY & RELIABILITY

Different Styles, Different Failure Toolkits

Failure handling in synchronous systems

TO

Timeouts

Never wait forever — always bound how long you’ll wait for a response.

RB

Retries with backoff

Retry transient failures, but with exponential backoff and jitter to avoid overwhelming a recovering service.

CB

Circuit breakers

Stop calling a service that is repeatedly failing, and fail fast instead, giving it room to recover.

BH

Bulkheads

Isolate thread pools per downstream dependency so one slow service can’t exhaust resources needed for calls to other services.

FB

Fallbacks / graceful degradation

Return cached or default data when the live call fails, rather than failing the whole user request.

JAVA · CIRCUIT BREAKER WITH RESILIENCE4J
@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);
    }
}

Failure handling in event-driven systems

RP

Broker replication

Kafka replicates each partition across multiple brokers (controlled by replication.factor), so a single broker failure doesn’t lose data.

A1

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).

DL

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.

OF

Consumer restart & offset tracking

If a consumer crashes, it resumes from its last committed offset rather than losing track of progress.

Idempotency example

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.

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).

10 · SECURITY

Protecting Both Wire And Broker

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.
  • Authorization: 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.

Securing event-driven integrations

  • Broker authentication: Producers and consumers authenticate to the broker itself (e.g., SASL/SCRAM or mTLS for Kafka).
  • Topic-level authorization (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.
Security consideration

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.

11 · MONITORING, LOGGING & METRICS

Two Styles, One Correlation ID

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.

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.
JAVA · PROPAGATING A CORRELATION ID INTO AN EVENT
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");
    }
}
Tip

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.

12 · DEPLOYMENT & CLOUD

Rolling Out Both Styles Safely

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.

Deploying event-driven infrastructure

  • Self-managed: Running Apache Kafka or RabbitMQ clusters on Kubernetes (via operators) or VMs, requiring careful capacity planning for disk, replication, and partition counts.
  • Managed cloud services: Amazon MSK / SQS / SNS, Google Cloud Pub/Sub, Azure Event Hubs, or Confluent Cloud, which offload 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.

Production example

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.

13 · DATABASES, CACHING & LOAD BALANCING

Where Data Lives For Each Style

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.

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.

Real-life analogy

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.

14 · APIS & MICROSERVICES

The Two Kinds Of Connective Tissue

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:

RG

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.

GW

API Gateway

As the single synchronous entry point for external clients, handling routing, authentication, and rate limiting before forwarding to internal services.

SD

Service discovery

So synchronous clients can find healthy instances of the service they need to call, without hardcoding IP addresses.

EB

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.

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.

Production example

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.

15 · DESIGN PATTERNS & ANTI-PATTERNS

Patterns That Age Well, Traps To Avoid

Useful patterns

RR

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.

TX

Transactional Outbox

Instead of writing to the database and publishing an event as two separate operations, the service writes the event into an “outbox” table in the same DB transaction as the business data. A separate CDC process (like Debezium) then reliably publishes rows from the outbox to the broker.

SG

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.

CB

Circuit Breaker

Covered earlier — protects synchronous callers from cascading failures.

CC

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:

  1. FlightReserved event published after the flight service succeeds.
  2. Hotel service consumes it, reserves the room, publishes HotelReserved.
  3. 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 Business row and event row are written in the same DB transaction, then CDC reliably forwards to the broker. Order Service writes Order + Outbox row Database orders table outbox table CDC Process Debezium · log tail reads outbox rows Message Broker delivers to all Consumers same DB txn reads log publishes
Fig 4 · Transactional Outbox — the industry-standard fix for the dual-write problem, giving you atomic “write + publish.”

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.
  • Using events where a direct answer is actually needed: 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.

16 · BEST PRACTICES & COMMON MISTAKES

A Portable Checklist

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.

Common mistakes

  • Treating “eventual consistency” as “the user won’t notice” without setting a realistic upper bound on how eventual is acceptable.
  • Forgetting that consumers can receive duplicate events and writing non-idempotent handlers.
  • Building synchronous chains so deep that a single slow dependency degrades the entire system.
  • Skipping dead-letter queues, so a single malformed event blocks an entire partition/queue indefinitely.
  • Not planning for schema evolution, leading to breaking changes that silently crash consumers in production.
Common misconception

“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.

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:

  1. 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.
  2. 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.
  3. Is the operation naturally a background task — something that doesn’t block the primary user journey? If yes, lean event-driven.
  4. 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.
  5. 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 tradeoff worthwhile. Starting simple and evolving deliberately, backed by real production metrics, is generally a better strategy than trying to predict every scaling requirement upfront.

17 · REAL-WORLD INDUSTRY EXAMPLES

The Big Platforms, Side By Side

CompanySynchronous UseEvent-Driven Use
NetflixFetching video manifests and license URLs before playback startsViewing activity, recommendations, and A/B test data flow through Kafka-based pipelines
AmazonChecking product availability and price at the moment of “Buy Now”Order fulfillment stages (packing, shipping, gift-wrap) coordinated via SQS/SNS queues
UberMatching a rider to a nearby driver and confirming the fare estimateLocation pings, trip-completion, and surge-pricing recalculation via Kafka streams
GoogleSearch query serving, requiring millisecond RPC responsesWeb crawling and indexing pipelines processed as large-scale asynchronous batch/event jobs

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, fulfillment 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.

ManifestNetflix’s synchronous “press play” call
SQSAmazon’s queue backbone for fulfillment
KafkaUber’s stream for trips & surge
RPCGoogle Search’s request-serving path

18 · FAQ, SUMMARY & KEY TAKEAWAYS

Answers, Wrap-Up And The One Idea To Remember

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.

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.

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.

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.

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.

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.

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.

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 tradeoff 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.

Leave a Reply

Your email address will not be published. Required fields are marked *