00:00

Communication in Microservices

In a microservices architecture, an application is split into many small, independent services. To do anything useful, such as placing an order, those services must talk to each other. How they communicate affects the speed, reliability and flexibility of the whole system.

This guide explains the two main communication styles, synchronous and asynchronous, with Java and Spring Boot examples, a complete e-commerce order flow, and practical advice on choosing protocols, handling failures and keeping data consistent.

1. What Is Microservices Communication?

Microservices communication is the set of mechanisms and protocols that independent services use to exchange information over a network. Each microservice is a self-contained unit with its own logic and its own database, so services must collaborate to complete business processes.

Think of it as an orchestra: each musician (microservice) plays their own part, and only by working together do they produce the symphony (the application).

Communication is not just a feature; it is the lifeline of a microservices architecture. Without it, you would have a collection of isolated programs unable to achieve anything together.

2. Why Communication Is Harder in Microservices

In a monolith, one part of the code calls another with a simple method call in the same process. In microservices, that same call travels over the network, which changes everything:

Method call in a monolithNetwork call between microservices
SpeedNanosecondsMilliseconds, often 100,000 times slower
Can it fail?Practically neverYes: timeouts, dropped connections, crashed services
DataObjects passed directlyData must be serialized, for example to JSON, and parsed again
TransactionsOne database transaction covers everythingNo shared transaction; each service has its own database
ChangesThe compiler checks both sidesBoth sides must agree on an API contract that can drift apart
DebuggingOne stack traceA request spans many services and logs

Good communication design is about accepting these facts and planning for them. The Communication Challenges in Microservices guide looks at each problem in detail.

3. The Two Primary Styles of Communication

Microservices communicate in two main ways, each with its own uses and trade-offs.

Synchronous (request–response)Asynchronous (event-driven)
How it worksThe caller sends a request and waits for the answerThe sender publishes a message and carries on without waiting
AnalogyA phone callA text message or a letter
Common technologiesHTTP/REST, gRPC, GraphQLApache Kafka, RabbitMQ, Amazon SQS and SNS
CouplingTight: both services must be running at the same timeLoose: the receiver can be offline and catch up later
Best forAnswers needed immediately, such as showing a page or checking stockBackground work and informing other services that something happened
Main drawbackChains of calls: one slow or failed service affects all callersMore moving parts and harder end-to-end tracking

3.1 Synchronous communication (request–response)

A client service sends a request and then waits for a response before it continues. It is a real-time “ask and wait for an answer” pattern.

  • How it works: the caller blocks, or waits, until it receives a response or the request times out.
  • Common protocols: HTTP/REST is the most widely used; gRPC is popular for its high performance.
  • Best for: situations where the result is needed right away, such as a user waiting for a page to load.
  • Drawback: it creates chains of dependencies. If one service is slow or down, the failure can cascade and make the whole request slow or unresponsive.

3.2 Asynchronous communication (event-driven)

A service (the publisher) sends a message but does not wait for a response. It announces that “something has happened”. Interested services (subscribers) receive the message and act on it in their own time.

  • How it works: a message broker, such as RabbitMQ, Apache Kafka or Amazon SQS, sits in the middle. The publisher sends a message to the broker, and the broker delivers it to the subscribers.
  • Common patterns: message queues and publish/subscribe (pub/sub).
  • Best for: background processing, tasks that do not need an instant result, and decoupling services to improve resilience.
  • Drawback: the system has more parts, and following a request from start to end takes extra tooling.

4. Synchronous Communication with REST and gRPC

4.1 Calling a REST API with RestClient

RestClient, available since Spring Framework 6.1 (Spring Boot 3.2), is the modern way to make synchronous HTTP calls in Spring. Always set timeouts, so a slow service cannot block your threads forever:

InventoryClient.java
package com.example.order.client;

import java.time.Duration;

import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

@Component
public class InventoryClient {

    private final RestClient restClient;

    public InventoryClient(RestClient.Builder builder) {
        SimpleClientHttpRequestFactory factory =
                new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(Duration.ofSeconds(2));
        factory.setReadTimeout(Duration.ofSeconds(3));

        this.restClient = builder
                .baseUrl("http://localhost:8082")
                .requestFactory(factory)
                .build();
    }

    public boolean reserve(String productId, int quantity) {
        ReservationResponse response = restClient.post()
                .uri("/inventory/reservations")
                .body(new ReservationRequest(productId, quantity))
                .retrieve()
                .body(ReservationResponse.class);

        return response != null && response.reserved();
    }

    public record ReservationRequest(String productId, int quantity) {
    }

    public record ReservationResponse(boolean reserved) {
    }
}

4.2 Declarative HTTP clients

Instead of writing each request by hand, you can describe the remote API as a Java interface and let Spring generate the implementation:

PaymentClient.java
package com.example.order.client;

import java.math.BigDecimal;

import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;

@HttpExchange("/payments")
public interface PaymentClient {

    @PostExchange
    PaymentResponse charge(@RequestBody PaymentRequest request);

    record PaymentRequest(String orderId, String customerId, BigDecimal amount) {
    }

    record PaymentResponse(String transactionId, String status) {
    }
}
ClientConfig.java
package com.example.order.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

import com.example.order.client.PaymentClient;

@Configuration
public class ClientConfig {

    @Bean
    public PaymentClient paymentClient(RestClient.Builder builder) {
        RestClient restClient = builder.baseUrl("http://localhost:8083").build();

        return HttpServiceProxyFactory
                .builderFor(RestClientAdapter.create(restClient))
                .build()
                .createClient(PaymentClient.class);
    }
}

Now you simply call paymentClient.charge(request). Spring Cloud OpenFeign offers a similar style and is still common in existing projects.

4.3 gRPC for fast internal calls

gRPC uses HTTP/2 and a compact binary format called Protocol Buffers. You describe the service in a .proto file, and tools generate client and server code in many languages:

inventory.proto
syntax = "proto3";

service InventoryService {
  rpc CheckStock (StockRequest) returns (StockReply);
}

message StockRequest {
  string product_id = 1;
  int32 quantity = 2;
}

message StockReply {
  bool available = 1;
}

gRPC is typically faster and smaller on the wire than REST with JSON, and it supports streaming. It is less convenient for browsers and for debugging with simple tools, so many teams use REST for public APIs and gRPC between internal services. Read more in Synchronous Communication in Microservices.

5. Asynchronous Communication with Message Brokers

5.1 Queues and topics

Message queue (point-to-point)Topic (publish/subscribe)
Who receives a message?One consumerEvery subscribed service
Typical useSharing work, such as “resize this image”Announcing events, such as “order confirmed”
AnalogyA ticket queue at a bank counterA radio broadcast

5.2 Publishing an event with Apache Kafka

An event describes something that has already happened. Name it in the past tense, and include the data subscribers will need:

OrderConfirmedEvent.java
package com.example.events;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;

public record OrderConfirmedEvent(
        String eventId,
        String orderId,
        String customerId,
        List<OrderLine> lines,
        BigDecimal total,
        Instant confirmedAt) {

    public record OrderLine(String productId, int quantity) {
    }
}
OrderEventPublisher.java
package com.example.order.messaging;

import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;

import com.example.events.OrderConfirmedEvent;

@Component
public class OrderEventPublisher {

    private static final String TOPIC = "order-confirmed";

    private final KafkaTemplate<String, OrderConfirmedEvent> kafkaTemplate;

    public OrderEventPublisher(KafkaTemplate<String, OrderConfirmedEvent> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public void publish(OrderConfirmedEvent event) {
        // The order ID is the key, so all events for one order keep their order
        kafkaTemplate.send(TOPIC, event.orderId(), event);
    }
}
application.yml (order service)
spring:
  kafka:
    bootstrap-servers: localhost:9092
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer

5.3 Subscribing to the event

ShippingEventListener.java
package com.example.shipping.messaging;

import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

import com.example.events.OrderConfirmedEvent;
import com.example.shipping.ShippingService;

@Component
public class ShippingEventListener {

    private final ShippingService shippingService;

    public ShippingEventListener(ShippingService shippingService) {
        this.shippingService = shippingService;
    }

    @KafkaListener(topics = "order-confirmed", groupId = "shipping-service")
    public void onOrderConfirmed(OrderConfirmedEvent event) {
        shippingService.createShipment(event.orderId(), event.lines());
    }
}
application.yml (shipping service)
spring:
  kafka:
    bootstrap-servers: localhost:9092
    consumer:
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      properties:
        spring.json.trusted.packages: "com.example.events"

The groupId matters. Every different group receives its own copy of each event, so the shipping service and the notification service (with groupId = "notification-service") both get every order. Several instances of the same group share the work instead, which is how a service scales out.

Version note: The JSON serializer class names shown are from Spring for Apache Kafka 3.x (Spring Boot 3). Newer versions also provide Jackson 3 based serializers; check the reference guide for your version. See Asynchronous Communication in Microservices and Event-Driven Architecture for more.

6. One-to-One and One-to-Many Interactions

Communication can also be described by how many receivers a message has:

PatternReceiversStyleExample
Request–responseOneSynchronousOrder service asks inventory: “Is product 123 in stock?”
Asynchronous request–responseOneAsynchronousSend “generate invoice” and receive “invoice ready” later on a reply queue
One-way notificationOneAsynchronous“Resize this uploaded image” put on a work queue
Publish/subscribeManyAsynchronous“Order confirmed” received by shipping, notifications and analytics

7. A Practical Example: The E-Commerce Order Flow

Let us walk through placing an order in an online store built with microservices.

Services involved:

  • Order service: manages order creation and status.
  • Inventory service: tracks product stock levels.
  • Payment service: handles card payments.
  • Shipping service: arranges delivery.
  • Notification service: sends emails and alerts to the customer.

7.1 Step 1: the user clicks “Place Order” (synchronous)

  1. The front end sends a request to the order service to create a new order.
  2. The order service makes a synchronous call to the inventory service: “Are 2 units of product 123 available?” Answer: “Yes, they are reserved for you.”
  3. It then makes another synchronous call to the payment service: “Charge this customer $50.00.” Answer: “Payment successful. Transaction ID: TXN789.”
  4. Only after both succeed does the order service mark the order as confirmed and reply to the browser: “Your order was successful!”

7.2 Step 2: handling the aftermath (asynchronous)

  1. The order service publishes an OrderConfirmed event to the message broker. Its job is now done; it does not know or care who listens.
  2. The shipping service receives the event, creates a shipping label and schedules a courier pickup.
  3. The notification service receives the same event and emails the customer an order summary.
Order flow
Browser ---> Order Service
               |  (sync)  ---> Inventory Service
               |  (sync)  ---> Payment Service
               |
               +--- OrderConfirmed event ---> [ Broker ]
                                                 |
                                   +-------------+-------------+
                                   |                           |
                           Shipping Service          Notification Service

7.3 The order service in code

OrderService.java
package com.example.order;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.UUID;

import org.springframework.stereotype.Service;

import com.example.events.OrderConfirmedEvent;
import com.example.order.client.InventoryClient;
import com.example.order.client.PaymentClient;
import com.example.order.client.PaymentClient.PaymentRequest;
import com.example.order.client.PaymentClient.PaymentResponse;
import com.example.order.messaging.OrderEventPublisher;

@Service
public class OrderService {

    private final InventoryClient inventoryClient;
    private final PaymentClient paymentClient;
    private final OrderRepository orderRepository;
    private final OrderEventPublisher eventPublisher;

    public OrderService(InventoryClient inventoryClient, PaymentClient paymentClient,
                        OrderRepository orderRepository, OrderEventPublisher eventPublisher) {
        this.inventoryClient = inventoryClient;
        this.paymentClient = paymentClient;
        this.orderRepository = orderRepository;
        this.eventPublisher = eventPublisher;
    }

    public Order placeOrder(PlaceOrderRequest request) {
        // Step 1a: synchronous - we need the answer before we can continue
        if (!inventoryClient.reserve(request.productId(), request.quantity())) {
            throw new OutOfStockException(request.productId());
        }

        // Step 1b: synchronous - the customer is waiting for the payment result
        Order order = orderRepository.save(Order.pending(request));
        BigDecimal amount = order.total();
        PaymentResponse payment = paymentClient.charge(
                new PaymentRequest(order.id(), request.customerId(), amount));

        order = orderRepository.save(order.confirm(payment.transactionId()));

        // Step 2: asynchronous - shipping and notifications happen in the background
        eventPublisher.publish(new OrderConfirmedEvent(
                UUID.randomUUID().toString(),
                order.id(),
                order.customerId(),
                order.lines(),
                order.total(),
                Instant.now()));

        return order;
    }
}

This hybrid approach is very common. Synchronous calls are used where an immediate answer is essential, and asynchronous events decouple the follow-up steps. If the shipping service is temporarily down, the OrderConfirmed event simply waits in the broker until it comes back, so a shipping problem never stops customers from placing orders.

Keep it simple, but know the gaps: This example leaves out what happens if payment fails after stock was reserved, or if the application crashes between saving the order and publishing the event. Section 12 explains the patterns that handle these cases.

8. How to Choose the Right Style

Ask yourselfIf yes, prefer
Does the caller need the answer to continue right now?Synchronous
Is a user waiting on screen for this exact result?Synchronous
Can the work happen a few seconds or minutes later?Asynchronous
Do several services need to react to the same thing?Asynchronous (publish/subscribe)
Must the work survive the receiver being down for a while?Asynchronous
Is the work slow or heavy, such as reports, video or bulk email?Asynchronous
Do you need to absorb sudden traffic spikes?Asynchronous (the queue acts as a buffer)

A good rule of thumb: query synchronously, react asynchronously. Ask for data you need now with a request, and announce changes with events.

9. Protocols and Data Formats Compared

ProtocolStyleData formatStrengthsTypical use
REST over HTTPSyncJSONSimple, universal, easy to debugPublic APIs and most service-to-service calls
gRPCSync and streamingProtocol Buffers (binary)Fast, compact, strongly typed contractsHigh-volume internal calls
GraphQLSyncJSONClients ask for exactly the fields they needAPIs for varied front ends
WebSocketTwo-wayAnyServer can push updates to clientsLive dashboards, chat
AMQP (RabbitMQ)AsyncAnyFlexible routing, per-message acknowledgmentTask queues and routing-heavy messaging
Apache KafkaAsyncAny (often JSON or Avro)Very high throughput; events are stored and can be replayedEvent streaming and event-driven systems
Cloud queues (SQS, Pub/Sub)AsyncAnyFully managed, no servers to runCloud-native background work

10. How Services Find Each Other

In the cloud, services start, stop and move all the time, so hard-coded addresses quickly break. Two building blocks solve this:

  • Service discovery: services register themselves, for example in Eureka, Consul or Kubernetes, and callers look them up by name, such as inventory-service.
  • API gateway: a single entry point for external clients that routes each request to the right service.

With message brokers, services do not need each other’s addresses at all; they only need to know the broker and the topic or queue name.

11. Making Communication Reliable

Networks fail, so every remote call needs protection. The most important tools are:

TechniqueProtects againstLearn more
TimeoutsWaiting forever for a slow serviceTimeout pattern
Retries with backoffShort, temporary failuresRetry pattern
Circuit breakerHammering a service that is already failingCircuit breaker pattern
BulkheadOne slow dependency using up all threadsBulkhead pattern
Rate limitingToo many requests overloading a serviceRate limiting pattern
IdempotencyDuplicate requests or messages causing duplicate effectsSection 11.2 below

11.1 Retry and circuit breaker with Resilience4j

StockChecker.java
package com.example.order.client;

import org.springframework.stereotype.Component;

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;

@Component
public class StockChecker {

    private final InventoryApi inventoryApi;

    public StockChecker(InventoryApi inventoryApi) {
        this.inventoryApi = inventoryApi;
    }

    // Reading stock is safe to retry; it changes nothing
    @Retry(name = "inventory")
    @CircuitBreaker(name = "inventory", fallbackMethod = "stockUnknown")
    public StockStatus check(String productId) {
        return inventoryApi.getStock(productId);
    }

    // Used when the inventory service is down or the circuit is open
    private StockStatus stockUnknown(String productId, Throwable ex) {
        return StockStatus.unknown(productId);
    }
}
application.yml
resilience4j:
  retry:
    instances:
      inventory:
        max-attempts: 3
        wait-duration: 500ms
  circuitbreaker:
    instances:
      inventory:
        sliding-window-size: 10
        failure-rate-threshold: 50
        wait-duration-in-open-state: 30s

These annotations need the resilience4j-spring-boot3 dependency (or Spring Cloud Circuit Breaker) together with Spring AOP.

11.2 Idempotent message handling

Most brokers guarantee at-least-once delivery: a message can arrive more than once, for example after a restart. Consumers must therefore be idempotent, meaning that handling the same message twice has the same effect as handling it once:

NotificationEventListener.java
package com.example.notification.messaging;

import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.example.events.OrderConfirmedEvent;
import com.example.notification.EmailService;
import com.example.notification.ProcessedEvent;
import com.example.notification.ProcessedEventRepository;

@Component
public class NotificationEventListener {

    private final ProcessedEventRepository processedEvents;
    private final EmailService emailService;

    public NotificationEventListener(ProcessedEventRepository processedEvents, EmailService emailService) {
        this.processedEvents = processedEvents;
        this.emailService = emailService;
    }

    @KafkaListener(topics = "order-confirmed", groupId = "notification-service")
    @Transactional
    public void onOrderConfirmed(OrderConfirmedEvent event) {
        // Skip events we have already handled
        if (processedEvents.existsById(event.eventId())) {
            return;
        }
        emailService.sendOrderConfirmation(event.customerId(), event.orderId());
        processedEvents.save(new ProcessedEvent(event.eventId()));
    }
}

12. Keeping Data Consistent Across Services

Each microservice owns its own database, so one database transaction cannot cover the whole order flow. Two patterns help:

  • Saga pattern: a business process is split into local steps, each with a compensating action. If payment fails after stock was reserved, the saga releases the reserved stock.
  • Transactional outbox: instead of saving the order and then publishing the event (two separate actions that can fail independently), the service saves the event into an outbox table in the same database transaction as the order. A separate process then publishes outbox rows to the broker. This avoids “order saved but event lost”.

The result is eventual consistency: all services agree after a short delay, rather than instantly. For most business processes, such as shipping after payment, this is perfectly acceptable. See Database per Service for why services keep separate data.

13. API Contracts and Versioning

Services evolve independently, so the contract between them, meaning the API or event format, must stay compatible:

  • Add, do not remove or rename. New optional fields are safe; removing or renaming fields breaks consumers.
  • Be tolerant when reading. Ignore unknown fields instead of failing, so producers can add fields freely.
  • Version breaking changes, for example /api/v2/orders or a new event type OrderConfirmedV2, and run old and new versions side by side for a while.
  • Document contracts with OpenAPI for REST, .proto files for gRPC and AsyncAPI or a schema registry for events.
  • Test contracts with consumer-driven contract tests, such as Spring Cloud Contract or Pact, so a provider change that breaks a consumer is caught before release.

14. Tracing Requests Across Services

When one user action touches five services, you need a way to follow it. Distributed tracing gives every request a trace ID that is passed along with each HTTP call and each message.

  • With Spring Boot, add Micrometer Tracing with an OpenTelemetry or Brave bridge; trace IDs are then passed automatically through RestClient, WebClient and Kafka.
  • Trace IDs appear in your log lines, so you can search all services’ logs for one request.
  • Tools such as Zipkin, Jaeger or Grafana Tempo show a timeline of every call, which makes slow steps easy to spot.

15. Best Practices

  1. Prefer asynchronous events for anything that does not need an immediate answer.
  2. Keep synchronous call chains short; avoid one request triggering a long chain of calls.
  3. Set a timeout on every remote call, and add retries and circuit breakers where they make sense.
  4. Retry only safe operations, or use idempotency keys for operations that change data.
  5. Make message consumers idempotent, because messages can arrive more than once.
  6. Design events carefully: past-tense names, a unique event ID, and the data subscribers need.
  7. Use the outbox pattern when saving data and publishing an event must both happen.
  8. Keep contracts backward compatible, and version breaking changes.
  9. Use service discovery rather than fixed addresses.
  10. Add distributed tracing and correlation IDs from the start.
  11. Secure internal traffic too, with tokens or mutual TLS, not only the public edge.

16. Common Mistakes to Avoid

MistakeWhat goes wrongBetter approach
Making everything synchronousA chain of calls is only as reliable as its weakest serviceUse events for follow-up work
No timeoutsThreads pile up waiting, and the caller crashes tooSet connect and read timeouts on every client
Retrying payments blindlyCustomers are charged twiceUse idempotency keys and retry only safe calls
Chatty servicesOne page needs dozens of calls, so it is slowReturn the data callers need in fewer calls, or aggregate
Sharing a database between servicesServices become tightly coupled through tablesEach service owns its data and exposes it through APIs or events
Events that carry too little dataEvery subscriber calls back to the publisher for detailsInclude the fields subscribers need
Ignoring duplicate messagesDuplicate emails, shipments or chargesTrack processed event IDs
Breaking API changes without versionsOther services fail after a deploymentAdd fields compatibly; version breaking changes

17. Summary

Microservices communication is the nervous system that lets a distributed application work as one. Synchronous communication, using REST or gRPC, suits requests that need an immediate answer. Asynchronous communication, using brokers such as Kafka or RabbitMQ, decouples services and keeps the system working when parts fail. Most real systems combine both: query synchronously, react asynchronously. Protect every call with timeouts, retries and circuit breakers, make consumers idempotent, keep data consistent with sagas and the outbox pattern, and trace requests across services. With these practices, your services stay independent while working together reliably. Continue with Synchronous Communication and Asynchronous Communication for deeper dives.