Microservices Architecture & Design

Synchronous vs Asynchronous Communication

18 min Lesson 5 of 12

Synchronous vs Asynchronous Communication

In a microservices architecture the question "how do services talk to each other?" is one of the most consequential design decisions you will make. Get it wrong and you end up with a distributed monolith — every service call blocks, failures cascade, and you lose the resilience benefits that microservices promised. This lesson covers the two fundamental styles — synchronous (request/response) and asynchronous (event/message) — explains their trade-offs, and shows you idiomatic Spring Boot 3 code for both.

Synchronous Communication: REST and gRPC

In synchronous communication the calling service sends a request and waits for the response before proceeding. The HTTP-based REST style you already know is the most common form. Spring Boot's RestClient (introduced in 6.1) and the older WebClient from WebFlux are the standard tools.

Consider an OrderService that must fetch product details from a ProductService before confirming an order:

// OrderService — calls ProductService synchronously via RestClient @Service public class OrderService { private final RestClient restClient; public OrderService(RestClient.Builder builder, @Value("${services.product.base-url}") String productBaseUrl) { this.restClient = builder.baseUrl(productBaseUrl).build(); } public OrderConfirmation placeOrder(OrderRequest request) { // Blocking HTTP GET — thread waits here ProductDto product = restClient.get() .uri("/api/products/{id}", request.productId()) .retrieve() .body(ProductDto.class); if (product == null || product.stock() < request.quantity()) { throw new InsufficientStockException(request.productId()); } Order order = orderRepository.save(new Order(request, product.price())); return new OrderConfirmation(order.getId(), product.name()); } }

The configuration for the base URL lives in application.yml so that different environments (local, staging, production) can wire in the real service address or a mock without touching code:

# application.yml services: product: base-url: http://product-service:8081
Service discovery: In a real cluster you would not hard-code product-service:8081. Spring Cloud LoadBalancer resolves the logical service name to a live instance via Eureka or Kubernetes DNS. The code stays identical — only the base URL changes to http://product-service and the @LoadBalanced annotation is added to the RestClient.Builder bean.

When Synchronous Communication Makes Sense

  • You need the result immediately — e.g., the response to the caller depends on data from another service.
  • The call is read-only and fast — fetching a product price for display is low risk; if it fails you can return an error or a cached value.
  • Strong consistency is required — you cannot proceed without knowing the current state of the remote resource.
  • Simple request/response semantics — a public-facing API gateway aggregating data from internal services is a natural fit.

The Hidden Cost: Temporal Coupling

Synchronous calls create temporal coupling: if ProductService is slow or unavailable, OrderService is also slow or broken. In a chain of three synchronous hops each with 99.9% availability, the composite availability drops to ~99.7%. With ten hops it falls below 99%. Add latency multiplication and thread exhaustion (every waiting thread holds a connection) and you see why pure synchronous chains are fragile at scale.

Never chain synchronous calls without a timeout and a circuit breaker. An upstream service that stops responding — but does not close the connection — will hold every thread in the downstream service hostage. Spring Cloud Circuit Breaker (backed by Resilience4j) is covered in the Resilience tutorial; always apply it to inter-service HTTP calls.

Asynchronous Communication: Messaging

In asynchronous communication the sender publishes a message (or event) to a broker and continues immediately — it does not wait for the receiver to process it. The receiver consumes the message at its own pace. This decouples services in time: the sender does not care whether the receiver is currently up, busy, or being redeployed.

Spring Boot integrates with Apache Kafka via spring-kafka and with RabbitMQ (AMQP) via spring-boot-starter-amqp. Below is a Kafka example: OrderService publishes an OrderPlaced event, and an independent InventoryService consumes it to decrement stock.

Add the dependency:

<!-- pom.xml --> <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> </dependency>

Define the event record (Java 16+ record, immutable by design):

public record OrderPlacedEvent(String orderId, String productId, int quantity, Instant occurredAt) {}

Publish from OrderService:

@Service public class OrderService { private final KafkaTemplate<String, OrderPlacedEvent> kafkaTemplate; private final OrderRepository orderRepository; // constructor injection omitted for brevity public String placeOrder(OrderRequest request) { Order order = orderRepository.save(new Order(request)); OrderPlacedEvent event = new OrderPlacedEvent( order.getId(), request.productId(), request.quantity(), Instant.now()); // Non-blocking: returns immediately; Kafka delivers asynchronously kafkaTemplate.send("orders.placed", order.getId(), event); return order.getId(); } }

Consume in InventoryService:

@Component public class InventoryEventHandler { private final InventoryRepository inventoryRepository; @KafkaListener(topics = "orders.placed", groupId = "inventory-service") public void onOrderPlaced(OrderPlacedEvent event) { inventoryRepository.decrementStock(event.productId(), event.quantity()); log.info("Stock decremented for product {} by {}", event.productId(), event.quantity()); } }

The Kafka broker, topic name, and serialization strategy are wired in application.yml:

spring: kafka: bootstrap-servers: kafka:9092 producer: key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: org.springframework.kafka.support.serializer.JsonSerializer consumer: key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer properties: spring.json.trusted.packages: "com.example.events"
Use a dead-letter topic (DLT). If onOrderPlaced throws an exception, Spring Kafka's DefaultErrorHandler can be configured to retry a fixed number of times and then forward the failed message to a orders.placed.DLT topic. Without a DLT, a poison-pill message will block the consumer forever.

When Asynchronous Communication Makes Sense

  • Fire-and-forget workflows — sending a confirmation email, updating a search index, notifying analytics. None of these need to block the user's request.
  • Eventual consistency is acceptable — the inventory will be updated soon after the order is placed; a tiny lag is fine.
  • High throughput or bursty load — the broker absorbs spikes; consumers process at a steady rate.
  • Long-running processes — video encoding, report generation, invoice PDFs — work that takes seconds or minutes must not block an HTTP thread.
  • Fan-out — one OrderPlaced event triggers inventory, invoicing, fulfilment, and analytics independently, without OrderService knowing about any of them.

Security Implications

Both styles have distinct security concerns that are easy to overlook in a microservices context.

Synchronous (HTTP): Propagate the caller's JWT downstream using an ExchangeFilterFunction on RestClient (or a ClientHttpRequestInterceptor). Never re-issue a service-level token that carries more privileges than the original user possessed — this is a privilege escalation vector.

// Propagate the Bearer token from the incoming request to outbound calls @Bean public RestClient.Builder restClientBuilder( ObjectProvider<HttpServletRequest> requestProvider) { return RestClient.builder() .requestInterceptor((request, body, execution) -> { HttpServletRequest incoming = requestProvider.getIfAvailable(); if (incoming != null) { String auth = incoming.getHeader(HttpHeaders.AUTHORIZATION); if (auth != null) request.getHeaders().set(HttpHeaders.AUTHORIZATION, auth); } return execution.execute(request, body); }); }

Asynchronous (Kafka/RabbitMQ): The broker is a network resource — treat it like a database. Use TLS for transport, SASL/SCRAM or mTLS for authentication, and ACLs so that OrderService can only produce to orders.* topics, not consume or administer them. Include a correlationId and a minimal userId claim in every event payload for audit logging, but do not embed full JWT tokens — they expire and are not meant for storage.

Choosing Between the Two: A Decision Framework

  • Ask: does the caller need the answer to continue? If yes → synchronous. If no → asynchronous.
  • Ask: can the receiver be temporarily unavailable? If it must always be up → synchronous with a circuit breaker. If it can catch up later → asynchronous.
  • Ask: how many services care about this event? One specific service → synchronous call. Many services → publish an event and let them subscribe.
  • Most real systems use both: a REST gateway for the user-facing request/response, with internal fan-out events for downstream side-effects.
The Saga pattern (covered in later lessons) coordinates multi-step business transactions across services using a choreography of asynchronous events — each service publishes a success or failure event and the next step reacts accordingly. It is the standard answer to distributed transactions and is only possible because of asynchronous messaging.

Summary

Synchronous REST/gRPC calls are simple and deliver immediate responses but create temporal coupling and cascading failure risk. Asynchronous messaging via Kafka or RabbitMQ decouples services in time, absorbs load spikes, and enables fan-out patterns, at the cost of eventual consistency and added infrastructure. Spring Boot 3 supports both styles with first-class integrations: RestClient for synchronous HTTP and spring-kafka / spring-amqp for messaging. The right choice depends on whether the caller needs the answer immediately, how many consumers exist, and what consistency guarantees the business requires.