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:
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:
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:
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:
A consumer in another service:
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
WebClientfor 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.

