Hook: In microservices, one service often has to call another before it can finish the user’s request — and that is exactly where slowdowns, outages, and bad retry logic hide.
Question: What is service-to-service communication in Spring Boot?
Answer: Service-to-service communication is when one Spring Boot microservice sends data to another microservice to complete a task. The most common form is a request/response HTTP call, but it can also be asynchronous with messages like Kafka events. The key idea is that the caller should set timeouts, handle errors, and keep the call small and focused.
Interview-Ready Answer: I think of service-to-service communication as one microservice depending on another to finish a business step. In Spring Boot, I would usually use RestClient, WebClient, or a declarative client like OpenFeign for HTTP calls, and I would set short timeouts, handle 4xx and 5xx responses explicitly, and add retries only for safe, idempotent reads. If the work is long-running or can be decoupled, I would prefer messaging like Kafka so one slow service does not block the whole request path.
Service-to-service communication means one backend service asks another backend service for help. In Spring Boot, that usually means an HTTP client inside one application calling an HTTP endpoint in another application, although the same idea also applies to events, queues, and gRPC. The important part is not the transport; it is the dependency: one service cannot finish until another service responds.
| Style | Best for | Main trade-off |
|---|---|---|
| Sync HTTP | Immediate replies | Blocks caller thread |
| WebClient | High concurrency | More reactive code |
| Messaging | Long tasks | Eventual consistency |
Spring Boot gives you a few practical choices. `RestTemplate` is the classic blocking client and still helps you understand the flow. `RestClient` is the newer blocking API in modern Spring, and `WebClient` is the non-blocking choice when you need more throughput. `OpenFeign` is a declarative wrapper that removes boilerplate, but it still makes the same underlying network call, so timeouts and retries still matter.
A remote call is O(1) in local code, but the real cost is network latency and waiting. Inside one cluster, a call often adds about 5 to 50 ms; across zones or regions it can be much more. A blocking client also holds one server thread while waiting, so five nested calls can pin five threads for one user request. If a service handles 200 threads and each downstream call waits 2 seconds, the path can top out around 100 requests per second before queueing, even if the CPU is not busy.
Two failure types show up again and again: hard failures, such as a 404 or 500, and slow failures, such as a timeout. Slow failures are often worse because they do not fail fast; they consume threads, pile up requests, and cause a cascade. The fix is not only code, but also resilience patterns: timeouts, retries with limits, circuit breakers, bulkheads, and good observability with metrics, logs, and trace IDs.
Imagine an e-commerce checkout service. Before it creates an order, it calls an inventory service to make sure the item is in stock, and it may also call pricing and shipping services. This is normal service-to-service communication: the checkout service owns the user flow, but it depends on other services for facts it does not own.
Now picture a bug: inventory becomes slow because its database index is missing after a deploy. Checkout keeps making synchronous HTTP calls with no timeout. Threads pile up, requests wait, and the site starts showing spinning loaders. In logs you see messages like Read timed out or 503 Service Unavailable, and in metrics the p95 latency jumps from 120 ms to several seconds. Users cannot finish payment even though the checkout code itself did not crash — it is stuck waiting on a dependency.
The fix is to fail fast, return a useful error, and decide whether a fallback is safe. For stock checks, a clear 503 is often better than letting the page hang forever. For email notifications, a queue would be better than a direct call because the user does not need to wait.
package com.example.servicetocommunication;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class ServiceToServiceCommunicationApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceToServiceCommunicationApplication.class, args);
}
}
@Configuration
class HttpClientConfig {
@Bean
RestTemplate inventoryRestTemplate(RestTemplateBuilder builder,
@Value("${service.inventory.base-url:http://localhost:8080}") String baseUrl) {
// Fail fast: a slow downstream should not keep a servlet thread busy forever.
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(1_000);
factory.setReadTimeout(2_000);
return builder
.rootUri(baseUrl)
.requestFactory(() -> factory)
.build();
}
}
record InventoryResponse(String sku, int available) {}
record OrderResponse(String sku, String status, int remaining) {}
@RestController
@RequestMapping("/inventory")
class InventoryController {
// In the real world this would come from a database or cache.
private final Map<String, Integer> stock = Map.of(
"book-1", 5,
"phone-2", 0,
"laptop-9", 2
);
@GetMapping("/{sku}")
public InventoryResponse getAvailability(@PathVariable String sku) {
Integer available = stock.get(sku);
if (available == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Unknown SKU: " + sku);
}
return new InventoryResponse(sku, available);
}
@GetMapping("/slow/{sku}")
public InventoryResponse slowAvailability(@PathVariable String sku) {
try {
// Simulate a dependency that is alive, but too slow for the timeout budget.
Thread.sleep(3_000);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Interrupted while simulating delay", ex);
}
Integer available = stock.get(sku);
if (available == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Unknown SKU: " + sku);
}
return new InventoryResponse(sku, available);
}
}
@RestController
@RequestMapping("/orders")
class OrderController {
private final RestTemplate inventoryRestTemplate;
OrderController(RestTemplate inventoryRestTemplate) {
this.inventoryRestTemplate = inventoryRestTemplate;
}
@PostMapping("/{sku}")
public ResponseEntity<OrderResponse> placeOrder(@PathVariable String sku) {
try {
// This is the service-to-service call: Order asks Inventory before it creates anything.
InventoryResponse inventory = inventoryRestTemplate.getForObject("/inventory/{sku}", InventoryResponse.class, sku);
if (inventory == null) {
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Inventory service returned no body");
}
if (inventory.available() < 1) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "Item is out of stock");
}
// In a real system you would now persist the order and probably reserve stock.
return ResponseEntity.ok(new OrderResponse(sku, "CREATED", inventory.available() - 1));
} catch (HttpClientErrorException.NotFound ex) {
// Translate a downstream 404 into a clean business error for the caller.
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "SKU not found in inventory", ex);
} catch (HttpServerErrorException ex) {
// Downstream 5xx should usually be treated as a temporary dependency failure.
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Inventory service failed: " + ex.getStatusCode(), ex);
} catch (ResourceAccessException ex) {
// Timeouts and connection failures are common distributed-system failures.
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Inventory service is unreachable or too slow", ex);
} catch (RestClientException ex) {
// Catch-all so unexpected client issues do not leak messy internals to the API boundary.
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Unexpected inventory client failure", ex);
}
}
@PostMapping("/slow/{sku}")
public ResponseEntity<OrderResponse> placeOrderAgainstSlowInventory(@PathVariable String sku) {
try {
InventoryResponse inventory = inventoryRestTemplate.getForObject("/inventory/slow/{sku}", InventoryResponse.class, sku);
if (inventory == null) {
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Inventory service returned no body");
}
if (inventory.available() < 1) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "Item is out of stock");
}
return ResponseEntity.ok(new OrderResponse(sku, "CREATED", inventory.available() - 1));
} catch (ResourceAccessException ex) {
// This demonstrates the failure path: the client times out instead of hanging forever.
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Request timed out while calling inventory", ex);
}
}
}
Follow-up & Tricky Questions:
Common Mistakes:
Memory Hook: Think phone call vs mailbox: synchronous service-to-service communication is a phone call, so both sides must stay online at the same time; asynchronous messaging is a mailbox, so the sender can drop the message and keep going.
Cheat Sheet:
Practice Tasks: