Synchronous Communication in Microservices
In a microservices architecture, an application is split into small, independent and loosely coupled services. To work together as one application, they must communicate, and they do so in two main ways: synchronously and asynchronously. This guide takes a deep look at synchronous communication.
You will learn how it works, walk through a complete order-processing example, and implement it in Spring Boot with RestClient, clear error responses, timeouts, parallel calls, gRPC, resilience patterns and tests. For a broader overview of both styles, see Communication in Microservices.
1. What Is Synchronous Communication?
Synchronous communication is a direct, real-time interaction between two services. The client service (the caller) sends a request and then waits for a response from the server service (the one doing the work) before it continues.
Think of it like a phone call. You dial, the other person answers, you talk (request and response), and you do not hang up until you have your answer. While the call lasts, your line is busy and you are actively waiting.
Order Service Inventory Service
|---- POST /api/inventory/reservations --->|
| (waiting...) | checks stock
|<--------- 200 OK { reserved: true } -----|
|
continues with the next step
2. Key Characteristics
| Characteristic | What it means |
|---|---|
| Request–response cycle | Every request is answered with a response, success or error |
| Blocking | The caller waits, usually holding a thread, until it gets a response or a timeout |
| Immediate feedback | The caller knows right away whether the request succeeded or failed |
| Temporal coupling | Both services must be running and reachable at the same moment |
3. How It Works: HTTP and REST
The most common protocol for synchronous communication is HTTP, usually with a REST API design. The client sends a request with a method (GET, POST, PUT, PATCH, DELETE) to an endpoint URL. The server processes it and returns a response with a status code and, usually, a JSON body.
| Status code | Meaning | Typical use between services |
|---|---|---|
200 OK / 201 Created | Success | Data returned, or a resource created |
400 Bad Request | The request itself is invalid | Missing or malformed fields |
404 Not Found | The resource does not exist | Unknown product or order ID |
409 Conflict | The request conflicts with the current state | Out of stock, already paid, duplicate request |
422 Unprocessable Content | Valid format, but a business rule rejects it | Payment declined, invalid quantity |
503 Service Unavailable | The server is temporarily unable to handle the request | Overload, maintenance; safe to retry later |
504 Gateway Timeout | A proxy did not get an answer in time | A downstream service was too slow |
Using the right status code matters: it tells the caller whether to show an error, try something else, or retry. Codes in the 4xx range mean “do not retry the same request”; 503 and 504 usually mean “try again later”.
4. A Detailed Example: E-Commerce Order Processing
Consider an e-commerce application with three microservices:
- Order service: creates and manages orders.
- Inventory service: manages product stock levels.
- Payment service: processes customer payments.
4.1 Step 1: the initial request
The user clicks Place Order. The front end sends a request to the order service to create a new order.
4.2 Step 2: reserving stock (synchronous call 1)
Before creating the order, the order service must make sure the items are in stock. It sends POST /api/inventory/reservations with the product IDs and quantities, and waits.
- Success:
200 OKwith a reservation ID. - Failure:
409 Conflictwith the message “Product X is out of stock”. The order service immediately tells the user that the order cannot be placed.
4.3 Step 3: processing payment (synchronous call 2)
With stock reserved, the order service sends POST /api/payments with a card token and the amount, and again waits. The payment service talks to the payment provider and replies.
- Success:
200 OKwith a transaction ID. - Failure:
422 Unprocessable Contentwith a reason, such as “card declined”. The order service releases the reserved stock and asks the user to try another payment method.
4.4 Step 4: finalizing the order
Only after both calls succeed does the order service save the order as CONFIRMED in its own database and return a success response to the front end.
Browser -> Order Service
|-- 1. reserve stock --> Inventory Service (wait)
|<--------- 200 OK ------
|-- 2. charge card ----> Payment Service (wait)
|<--------- 200 OK ------
|-- 3. save order (CONFIRMED)
Browser <- 201 Created
402 Payment Required for payment problems. That code is officially reserved for future use, so a clear 422 or 409 with an explanatory message is more common between microservices.5. Choosing an HTTP Client in Spring
| Client | Style | When to use it |
|---|---|---|
RestClient | Synchronous, fluent API (Spring Framework 6.1+) | The default choice for new blocking code |
HTTP interfaces (@HttpExchange) | Declarative: describe the API as a Java interface | Many endpoints; clean, testable client code |
WebClient | Reactive, non-blocking | Spring WebFlux applications, or streaming |
| OpenFeign | Declarative (Spring Cloud) | Existing projects already using it |
RestTemplate | Older synchronous API | Legacy code only; prefer RestClient for new work |
This guide uses RestClient. HTTP interfaces can be built on top of it when you prefer a declarative style.
6. Implementing the Order Flow with RestClient
6.1 The inventory client
The client turns HTTP details into simple Java methods and translates error codes into meaningful exceptions:
package com.example.order.client;
import java.util.List;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
@Component
public class InventoryClient {
private final RestClient restClient;
public InventoryClient(RestClient.Builder builder) {
this.restClient = builder.baseUrl("http://inventory-service").build();
}
public String reserve(String orderId, List<OrderLine> lines) {
ReservationResponse response = restClient.post()
.uri("/api/inventory/reservations")
.body(new ReservationRequest(orderId, lines))
.retrieve()
// 409 means "out of stock": a business outcome, not a technical error
.onStatus(status -> status.value() == 409, (request, resp) -> {
throw new OutOfStockException("Some items in order " + orderId + " are out of stock");
})
.body(ReservationResponse.class);
return response.reservationId();
}
public void release(String reservationId) {
restClient.delete()
.uri("/api/inventory/reservations/{id}", reservationId)
.retrieve()
.toBodilessEntity();
}
public record OrderLine(String productId, int quantity) {
}
record ReservationRequest(String orderId, List<OrderLine> lines) {
}
record ReservationResponse(String reservationId) {
}
}
The payment client follows the same shape, translating 422 into a PaymentDeclinedException. Other 4xx and 5xx responses become Spring’s HttpClientErrorException and HttpServerErrorException automatically.
6.2 The order service
package com.example.order;
import org.springframework.stereotype.Service;
import com.example.order.client.InventoryClient;
import com.example.order.client.PaymentClient;
import com.example.order.client.PaymentDeclinedException;
@Service
public class OrderService {
private final InventoryClient inventoryClient;
private final PaymentClient paymentClient;
private final OrderRepository orders;
public OrderService(InventoryClient inventoryClient, PaymentClient paymentClient,
OrderRepository orders) {
this.inventoryClient = inventoryClient;
this.paymentClient = paymentClient;
this.orders = orders;
}
public Order placeOrder(PlaceOrderRequest request) {
Order order = orders.save(Order.pending(request));
// Synchronous call 1: throws OutOfStockException if stock is missing
String reservationId = inventoryClient.reserve(order.getId(), request.lines());
try {
// Synchronous call 2
String transactionId = paymentClient.charge(order.getId(), request.cardToken(), order.getTotal());
order.confirm(transactionId);
} catch (PaymentDeclinedException ex) {
// Undo step 1 so the stock can be sold to someone else
inventoryClient.release(reservationId);
order.reject(ex.getMessage());
orders.save(order);
throw ex;
}
return orders.save(order);
}
}
Notice the manual “undo” when payment fails. With only two steps this is manageable, but longer chains of synchronous calls that each need undoing are a sign that the Saga pattern with events is a better fit.
7. Returning Clear Errors with ProblemDetail
The server side should return errors that clients can understand. Spring supports the standard Problem Details format (RFC 9457) through the ProblemDetail class:
package com.example.inventory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ReservationController {
private final StockService stock;
public ReservationController(StockService stock) {
this.stock = stock;
}
@PostMapping("/api/inventory/reservations")
public ResponseEntity<?> reserve(@RequestBody ReservationRequest request) {
return stock.tryReserve(request)
.<ResponseEntity<?>>map(reservationId -> ResponseEntity.ok(new ReservationResponse(reservationId)))
.orElseGet(() -> {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.CONFLICT, "One or more items are out of stock");
problem.setTitle("Out of stock");
return ResponseEntity.of(problem).build();
});
}
}
{
"type": "about:blank",
"title": "Out of stock",
"status": 409,
"detail": "One or more items are out of stock",
"instance": "/api/inventory/reservations"
}
A consistent error format across all services makes client code simpler and logs easier to read. Never return stack traces or internal details to other services.
8. Timeouts: The Most Important Setting
Without timeouts, a synchronous call to a hung service can wait for minutes, holding a thread the whole time. Set timeouts once for every client with a RestClientCustomizer:
package com.example.order.config;
import java.time.Duration;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
@Configuration
public class HttpClientConfig {
@Bean
public RestClientCustomizer timeouts() {
return builder -> {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofSeconds(1)); // time to open a connection
factory.setReadTimeout(Duration.ofSeconds(3)); // time to wait for the response
builder.requestFactory(factory);
};
}
}
Every RestClient.Builder that Spring injects now has these timeouts. Choose values from real response times: slightly above the slowest normal response, and well below what users will accept. See the Timeout pattern for more.
9. Blocking Threads and Virtual Threads
In a classic Spring MVC application, each incoming request uses one thread from a pool, often 200 threads in Tomcat. During a synchronous call, that thread just waits. This limits throughput when downstream services are slow:
200 threads, each request waits on a downstream call
Downstream answers in 0.1 s -> up to ~2,000 requests/second
Downstream answers in 10 s -> up to ~20 requests/second
(all threads busy waiting)
With Java 21 and Spring Boot 3.2 or later, you can use virtual threads: very lightweight threads that the JVM can create in large numbers. Waiting no longer ties up a scarce platform thread:
# Handle each request on a virtual thread (Java 21+)
spring.threads.virtual.enabled=true
Virtual threads make blocking code scale much better, but they do not make a slow service faster, and they do not protect the downstream service from too many concurrent calls. You still need timeouts, and limits such as the Bulkhead pattern.
10. Making Independent Calls in Parallel
When calls do not depend on each other, run them at the same time. The total time becomes the time of the slowest call instead of the sum of all calls:
@Service
public class ProductPageService {
private final CatalogClient catalogClient;
private final ReviewClient reviewClient;
private final InventoryClient inventoryClient;
private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
public ProductPageService(CatalogClient catalogClient, ReviewClient reviewClient,
InventoryClient inventoryClient) {
this.catalogClient = catalogClient;
this.reviewClient = reviewClient;
this.inventoryClient = inventoryClient;
}
public ProductPage load(String productId) {
var product = CompletableFuture.supplyAsync(() -> catalogClient.get(productId), executor);
var reviews = CompletableFuture.supplyAsync(() -> reviewClient.top(productId), executor);
var stock = CompletableFuture.supplyAsync(() -> inventoryClient.stockLevel(productId), executor);
// About max(t1, t2, t3) instead of t1 + t2 + t3
return new ProductPage(product.join(), reviews.join(), stock.join());
}
}
Add a timeout and fallback to each future for optional data, such as reviews, so one slow service does not delay the whole page. The Aggregator pattern covers this in detail.
11. Synchronous Calls with gRPC
gRPC is a high-performance alternative to REST. It uses HTTP/2 and a compact binary format, Protocol Buffers. You describe the service in a .proto file, and a build plugin generates Java client and server classes:
syntax = "proto3";
option java_package = "com.example.inventory.grpc";
option java_multiple_files = true;
service InventoryService {
rpc CheckStock (StockRequest) returns (StockReply);
}
message StockRequest {
string product_id = 1;
int32 quantity = 2;
}
message StockReply {
bool available = 1;
int32 in_stock = 2;
}
package com.example.order.client;
import java.util.concurrent.TimeUnit;
import com.example.inventory.grpc.InventoryServiceGrpc;
import com.example.inventory.grpc.StockReply;
import com.example.inventory.grpc.StockRequest;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
public class GrpcInventoryClient {
private final InventoryServiceGrpc.InventoryServiceBlockingStub stub;
public GrpcInventoryClient(String host, int port) {
ManagedChannel channel = ManagedChannelBuilder.forAddress(host, port)
.usePlaintext() // use TLS in production
.build();
this.stub = InventoryServiceGrpc.newBlockingStub(channel);
}
public boolean isAvailable(String productId, int quantity) {
StockReply reply = stub
.withDeadlineAfter(2, TimeUnit.SECONDS) // gRPC's timeout
.checkStock(StockRequest.newBuilder()
.setProductId(productId)
.setQuantity(quantity)
.build());
return reply.getAvailable();
}
}
| REST + JSON | gRPC | |
|---|---|---|
| Format | Text (JSON), easy to read | Binary (Protocol Buffers), compact |
| Contract | Optional (OpenAPI) | Required (.proto), code generated |
| Performance | Good | Faster, smaller messages |
| Streaming | Limited | Built in, in both directions |
| Browser support | Native | Needs a proxy (gRPC-Web) |
| Best for | Public APIs and most internal calls | High-volume internal calls between services |
12. Retries, Circuit Breakers and Fallbacks
Because synchronous calls depend on the other service being available, protect them with resilience patterns:
@Retry(name = "payment")
@CircuitBreaker(name = "payment", fallbackMethod = "paymentUnavailable")
public String charge(String orderId, String cardToken, BigDecimal amount) {
return restClient.post()
.uri("/api/payments")
.header("Idempotency-Key", orderId) // safe to retry: never charges twice
.body(new ChargeRequest(orderId, cardToken, amount))
.retrieve()
.body(ChargeResponse.class)
.transactionId();
}
private String paymentUnavailable(String orderId, String cardToken, BigDecimal amount, Exception ex) {
throw new ServiceTemporarilyUnavailableException("Payments are temporarily unavailable. Please try again.");
}
| Pattern | Protects against |
|---|---|
| Timeout | Waiting forever for a slow service |
| Retry | Short, temporary failures (only for idempotent requests) |
| Circuit breaker | Repeatedly calling a service that is down |
| Bulkhead | One slow dependency using up all threads |
| Fallback | Showing users a raw error when a call fails |
13. Testing Synchronous Clients
Spring Boot’s @RestClientTest starts only what a client needs, plus a mock server that answers instead of the real service:
package com.example.order.client;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
@RestClientTest(InventoryClient.class)
class InventoryClientTest {
private static final String URL = "http://inventory-service/api/inventory/reservations";
@Autowired
private InventoryClient client;
@Autowired
private MockRestServiceServer server;
@Test
void returnsReservationIdWhenStockIsAvailable() {
server.expect(requestTo(URL))
.andExpect(method(HttpMethod.POST))
.andRespond(withSuccess("{\"reservationId\":\"R-77\"}", MediaType.APPLICATION_JSON));
String id = client.reserve("1001", List.of(new InventoryClient.OrderLine("P-11", 2)));
assertThat(id).isEqualTo("R-77");
}
@Test
void throwsOutOfStockOn409() {
server.expect(requestTo(URL))
.andRespond(withStatus(HttpStatus.CONFLICT));
assertThatThrownBy(() -> client.reserve("1001", List.of(new InventoryClient.OrderLine("P-11", 99))))
.isInstanceOf(OutOfStockException.class);
}
}
For full integration tests, tools such as WireMock can simulate slow responses and failures, and contract tests, for example with Spring Cloud Contract or Pact, check that client and server still agree on the API. The test annotation packages moved in Spring Boot 4; the code itself stays the same.
14. Advantages and Disadvantages
| Advantages | Disadvantages |
|---|---|
| Simplicity: easy to understand, like calling a function | Availability dependency: if the inventory service is down, no orders can be placed, even though the order service is healthy |
| Immediate error handling: the caller knows at once if something failed | Cascading delays: a chain is only as fast and reliable as its weakest link |
| Easy to reason about: steps happen in a clear order | Temporal coupling: both services must be available at the same time |
| Great tooling: HTTP is understood everywhere and easy to debug | Resource pressure: waiting threads can be exhausted under load |
15. When to Use Synchronous vs Asynchronous Communication
| Situation | Prefer |
|---|---|
| A user is waiting for this exact answer | Synchronous |
| The caller needs data to decide its next step | Synchronous |
| Several services must react to something that happened | Asynchronous (events) |
| The work is slow, such as reports or emails | Asynchronous |
| The receiving service may be offline for a while | Asynchronous |
| Traffic comes in big spikes that need smoothing | Asynchronous (a queue buffers it) |
A well-designed system uses both: synchronous calls for what the user waits for, and events for everything that can happen afterwards. See Asynchronous Communication in Microservices.
16. Best Practices
- Set connect and read timeouts on every client, in one central place.
- Keep call chains short: ideally no more than two or three synchronous hops per user request.
- Call independent services in parallel.
- Use meaningful status codes and a consistent error format such as Problem Details.
- Translate errors at the client boundary into clear exceptions, such as
OutOfStockException. - Retry only idempotent requests, or use idempotency keys.
- Add circuit breakers and fallbacks for important dependencies.
- Use service discovery instead of fixed addresses.
- Pass trace IDs along with every call, so a request can be followed across services.
- Version APIs and keep changes backward compatible.
17. Common Mistakes to Avoid
| Mistake | What goes wrong | Better approach |
|---|---|---|
| No timeouts | A hung service blocks threads until the caller also fails | Set connect and read timeouts |
| Long synchronous chains | Latency adds up and any failure breaks the whole request | Use events for follow-up steps |
| Wrong status codes, such as 200 with an error body | Callers cannot react correctly | Use proper 4xx/5xx codes |
| Retrying non-idempotent POST requests | Duplicate orders or payments | Use idempotency keys |
| Calling services one after another when they are independent | Slow pages | Call them in parallel |
| Exposing internal errors and stack traces | Leaks details and confuses callers | Return Problem Details with a clear message |
| Sharing request/response classes between services | Services must be released together | Each client defines its own small records |
18. Summary
Synchronous communication, usually HTTP/REST or gRPC, is the natural choice when a caller needs an immediate answer, especially for user-facing operations. It is simple and gives instant feedback, but its blocking nature and temporal coupling make it fragile if used carelessly. In Spring Boot, build clients with RestClient, translate error codes into clear exceptions, return Problem Details from services, set timeouts everywhere, use virtual threads and parallel calls for better throughput, and protect important calls with retries, circuit breakers and fallbacks. Keep synchronous chains short, and move follow-up work to asynchronous events. A thoughtful mix of both styles balances responsiveness, resilience and simplicity. Continue with Asynchronous Communication to complete the picture.
19. Frequently Asked Questions (FAQ)
Q1. What is synchronous communication in microservices?
It is a style of communication in which one service sends a request and waits for the answer before continuing. The most common example is an HTTP REST call: the order service asks the inventory service “is this in stock?” and waits for the reply.
It works like a phone call: both sides must be available at the same time, and the caller stays on the line until it gets an answer.
Q2. Is REST always synchronous?
The HTTP request itself is request–response, so REST calls are synchronous from the protocol’s point of view. But REST can also start asynchronous work:
- The server replies
202 Acceptedimmediately and processes the job in the background. - The client checks a status URL later, or receives a callback (webhook) when the job is done.
This is useful for long-running tasks such as report generation.
Q3. Is a non-blocking WebClient call still synchronous communication?
Yes, at the architecture level. With WebClient, your thread is not blocked while waiting, which uses resources more efficiently. But the caller still needs the response to continue, and both services must be available at the same time.
“Non-blocking” describes how your code waits; “asynchronous communication” in microservices usually means messages through a broker, where the receiver does not have to be online.
Q4. Which HTTP client should I use in Spring Boot?
RestClient: the default for new synchronous code (Spring Framework 6.1 and later).- HTTP interfaces with
@HttpExchange: when you prefer to describe the remote API as a Java interface. WebClient: in reactive WebFlux applications.RestTemplate: only in existing code; it is in maintenance mode.
Q5. What timeout values should I use?
There is no universal number, but common starting points are:
- Connect timeout: about 1–2 seconds; inside a data center, connections should open almost instantly.
- Read timeout: a little above the slowest normal response time of that service, often 2–5 seconds.
Measure real response times, especially the 99th percentile, and keep the total time of a user request within what users will accept.
Q6. What happens if a synchronous call has no timeout?
If the other service hangs, the calling thread can wait for minutes. As more requests arrive, more threads wait, until the caller has no free threads and stops responding too. One stuck service can then bring down every service that calls it.
This is one of the most common causes of cascading failures. Always set timeouts.
Q7. Should I use REST or gRPC between microservices?
Use REST by default: it is simple, human-readable and supported everywhere. Choose gRPC when:
- Services exchange a very high volume of calls and message size or speed matters.
- You want strict, generated contracts across several languages.
- You need streaming in one or both directions.
Many systems use REST for public APIs and gRPC for busy internal paths.
Q8. How many synchronous calls can one request safely make?
As a guide, keep it to two or three hops in a chain, such as client → gateway → service → one other service. Every extra hop adds latency and another place where the request can fail.
If a request needs many services, call independent ones in parallel, keep local copies of frequently needed data, or move follow-up steps to asynchronous events.
Q9. What are virtual threads, and do they help?
Virtual threads (Java 21+) are lightweight threads managed by the JVM. Thousands of them can wait on network calls without using up the operating system’s threads. With Spring Boot 3.2+, enable them with spring.threads.virtual.enabled=true.
They help blocking, synchronous code handle many more concurrent requests. They do not make slow services faster, so timeouts and limits on concurrent calls are still needed.
Q10. How should a client handle error responses?
- 4xx errors (such as 400, 404, 409, 422): the request is wrong or rejected. Do not retry; translate the error into a clear exception or message.
- 503 and 504, timeouts and connection errors: the problem may be temporary. Retry a limited number of times if the operation is idempotent.
- 500: often a bug; retry at most once and log it.
With RestClient, use onStatus(...) to map specific codes to your own exceptions.
Q11. What is the Problem Details format?
Problem Details (RFC 9457) is a standard JSON format for HTTP errors, with fields such as type, title, status, detail and instance.
Spring supports it through the ProblemDetail class and can produce it automatically for many errors. Using one format across all services makes error handling in clients much simpler.
Q12. How do I avoid duplicate actions when a synchronous call is retried?
Make the operation idempotent. For actions that change data, such as payments, send an idempotency key, for example the order ID, in a header:
POST /api/payments
Idempotency-Key: order-1001
The server remembers keys it has processed and returns the original result for repeats, so the customer is charged only once even if the request is sent several times.
Q13. What is temporal coupling?
Temporal coupling means two services must be running at the same time for an operation to succeed. Synchronous communication always creates it: if the inventory service is down, the order service cannot reserve stock, even though the order service itself is healthy.
Asynchronous messaging removes temporal coupling, because a broker stores messages until the receiver is back.
Q14. How do I call several services at the same time?
Start each independent call as a CompletableFuture, ideally on virtual threads, then wait for all of them:
var product = CompletableFuture.supplyAsync(() -> catalogClient.get(id), executor);
var reviews = CompletableFuture.supplyAsync(() -> reviewClient.top(id), executor);
ProductPage page = new ProductPage(product.join(), reviews.join());
The total time is about the time of the slowest call. Add timeouts and fallbacks for optional data.
Q15. How do services find each other's addresses for synchronous calls?
Through service discovery. Services call a name, such as http://inventory-service, instead of an IP address:
- On Kubernetes, the Service name is resolved by DNS.
- With Eureka or Consul, a
@LoadBalancedRestClient.Builderreplaces the name with a real instance address.
Q16. Should the API gateway make synchronous calls to services?
Yes. The gateway receives a client request, forwards it synchronously to the right service, and returns the response. Treat it like any other client:
- Set timeouts on every route.
- Add circuit breakers for important services.
- Keep it thin; avoid long chains of calls inside the gateway.
Q17. How do I secure synchronous calls between services?
- Use HTTPS/TLS, even inside the network, or mutual TLS through a service mesh.
- Forward the user’s token (JWT) when acting on behalf of a user.
- Use service credentials, such as OAuth 2 client-credential tokens, for service-to-service calls.
- Let each service authorize requests itself, not only the gateway.
Q18. How do I trace a request across synchronous calls?
Use distributed tracing. With Micrometer Tracing in Spring Boot, a trace ID is created for each incoming request and automatically passed along in HTTP headers by RestClient and WebClient built from Spring’s builders.
The trace ID appears in every service’s logs, and tools such as Zipkin, Jaeger or Grafana Tempo show a timeline of all calls, making slow steps easy to spot.
Q19. How do I test code that makes synchronous calls?
@RestClientTestwithMockRestServiceServer: fast tests of a single client.- WireMock: realistic HTTP stubs, including delays and errors, for integration tests.
- Contract tests, such as Spring Cloud Contract or Pact: check that client and server still agree.
- A few end-to-end tests for the most important flows.
Q20. When should I switch from synchronous to asynchronous communication?
Consider events or messaging when:
- The caller does not need the result right away.
- Several services must react to the same change.
- The work is slow or comes in large spikes.
- A downstream service being offline should not block the caller.
- Synchronous chains have become long and fragile.
Keep synchronous calls for what a user is actively waiting for.