Spring Boot
microservices
communication
inter-service communication
Java

Spring Boot - how to communicate between microservices?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Spring Boot does not impose a single communication style for microservices. Instead, it gives you the building blocks to choose between synchronous calls, asynchronous messaging, and infrastructure features such as discovery, retries, and observability. The correct choice depends on latency requirements, failure tolerance, and how tightly the services are allowed to depend on each other.

Choosing Between Synchronous and Asynchronous Calls

The most common communication patterns are:

  • synchronous request-response, usually HTTP or gRPC
  • asynchronous messaging through a broker such as Kafka or RabbitMQ

Synchronous communication is easier to reason about because the caller gets an immediate response. The downside is runtime coupling: if order-service calls inventory-service, the caller must wait for the callee to be healthy and fast enough.

Asynchronous communication reduces that coupling. A service publishes an event or sends a command, then continues its own work. This improves resilience, but the system becomes eventually consistent rather than immediately consistent.

In practice:

  • use synchronous calls when the caller truly needs the answer before continuing
  • use asynchronous messaging when work can happen later or be retried independently

Calling Another Service with WebClient

For HTTP-based communication in modern Spring Boot applications, WebClient is preferred over RestTemplate for new development. It supports both reactive and blocking usage, has better extension points, and works well with timeouts and retries.

An example service that calls a pricing service:

java
1import java.math.BigDecimal;
2import java.time.Duration;
3import org.springframework.http.MediaType;
4import org.springframework.stereotype.Service;
5import org.springframework.web.reactive.function.client.WebClient;
6
7@Service
8public class PricingClient {
9    private final WebClient webClient;
10
11    public PricingClient(WebClient.Builder builder) {
12        this.webClient = builder
13                .baseUrl("http://pricing-service")
14                .build();
15    }
16
17    public BigDecimal fetchPrice(String sku) {
18        return webClient.get()
19                .uri(uriBuilder -> uriBuilder
20                        .path("/prices/{sku}")
21                        .build(sku))
22                .accept(MediaType.APPLICATION_JSON)
23                .retrieve()
24                .bodyToMono(BigDecimal.class)
25                .timeout(Duration.ofSeconds(2))
26                .block();
27    }
28}

The service above still behaves synchronously because of .block(). That is acceptable in a regular MVC application, but the timeout is essential. Without it, a slow downstream service can tie up threads until the entire application starts failing under load.

Exposing a Simple REST Endpoint

The service being called can be a normal Spring Boot controller:

java
1import java.math.BigDecimal;
2import org.springframework.web.bind.annotation.GetMapping;
3import org.springframework.web.bind.annotation.PathVariable;
4import org.springframework.web.bind.annotation.RestController;
5
6@RestController
7public class PriceController {
8
9    @GetMapping("/prices/{sku}")
10    public BigDecimal price(@PathVariable String sku) {
11        if ("ABC-123".equals(sku)) {
12            return new BigDecimal("19.99");
13        }
14        return BigDecimal.ZERO;
15    }
16}

This is the simplest inter-service contract: one service exposes HTTP, another consumes it. The risk is not the code itself, but the operational behavior around latency, retries, and versioning.

Service Discovery and Stable Addresses

In local development, hard-coded URLs are often enough. In a real microservice environment, instances move, scale, and restart. That means service-to-service communication should usually rely on stable discovery or routing infrastructure.

Typical choices are:

  • Kubernetes service DNS such as http://pricing-service
  • a gateway that routes external traffic
  • a service registry in older Spring Cloud setups

The article question is often asked as if communication is only about client code, but the addressing strategy matters just as much. Without stable service discovery, every deployment becomes a manual reconfiguration problem.

Adding Resilience Around Remote Calls

A microservice call should assume the network will fail. In Spring Boot, common protections include:

  • connection and read timeouts
  • retries for transient failures only
  • circuit breaking to stop hammering an unhealthy dependency
  • fallbacks where partial degradation is acceptable

A simple retry policy with Reactor operators looks like this:

java
1import reactor.util.retry.Retry;
2import java.time.Duration;
3
4public BigDecimal fetchPriceWithRetry(String sku) {
5    return webClient.get()
6            .uri("/prices/{sku}", sku)
7            .retrieve()
8            .bodyToMono(BigDecimal.class)
9            .timeout(Duration.ofSeconds(2))
10            .retryWhen(Retry.backoff(3, Duration.ofMillis(200)))
11            .block();
12}

Be careful here: retrying every failure is wrong. A 404 or validation error is not transient and should not be retried. Retries help with timeouts or temporary 5xx responses, not bad requests.

Using Messaging for Looser Coupling

If order-service only needs to notify other services that an order was created, messaging is often a better fit than direct HTTP. The caller publishes an event, and downstream consumers react independently.

Example with Spring for Apache Kafka:

java
1import org.springframework.kafka.core.KafkaTemplate;
2import org.springframework.stereotype.Service;
3
4@Service
5public class OrderEventPublisher {
6    private final KafkaTemplate<String, String> kafkaTemplate;
7
8    public OrderEventPublisher(KafkaTemplate<String, String> kafkaTemplate) {
9        this.kafkaTemplate = kafkaTemplate;
10    }
11
12    public void publishCreated(String orderId) {
13        kafkaTemplate.send("orders.created", orderId);
14    }
15}

A consumer in another service:

java
1import org.springframework.kafka.annotation.KafkaListener;
2import org.springframework.stereotype.Component;
3
4@Component
5public class BillingListener {
6
7    @KafkaListener(topics = "orders.created", groupId = "billing")
8    public void handle(String orderId) {
9        System.out.println("Create invoice for order " + orderId);
10    }
11}

This pattern reduces direct dependency between services, but it requires careful event design, idempotency, and monitoring. Consumers may see duplicate messages, delayed messages, or messages arriving in unexpected sequences.

Versioning and Contracts

Microservices fail when teams change payloads without a contract strategy. Keep communication stable by:

  • versioning externally visible APIs when breaking changes are unavoidable
  • adding fields in a backward-compatible way
  • documenting events and HTTP responses clearly
  • testing consumer expectations in CI

Even when both services are owned by the same team, silent contract drift creates production breakage quickly.

Common Pitfalls

  • Calling downstream services without timeouts, which turns a slow dependency into thread exhaustion.
  • Using synchronous HTTP for workflows that would be safer as events or queued work.
  • Retrying non-transient failures such as bad requests or missing records.
  • Hard-coding service addresses that change across environments or deployments.
  • Treating message consumers as exactly-once handlers when duplicates are still possible.

Summary

  • Spring Boot supports both synchronous HTTP and asynchronous messaging between microservices.
  • Use WebClient for modern HTTP client code and always configure timeouts.
  • Rely on service discovery or stable routing instead of environment-specific hard-coded URLs.
  • Add resilience features such as retries and circuit breaking only where they make sense.
  • Prefer messaging when services can be decoupled and eventual consistency is acceptable.

Course illustration
Course illustration

All Rights Reserved.