Skip to content
Spring Boot sb microservices 3 min read

Inter-Service Communication

Once you split a system into services, the calls between them become your most important design decisions. The first choice is synchronous (request/response over HTTP) versus asynchronous (events over a broker). Each has sharp trade-offs around coupling, consistency, and failure. This page covers both styles in Spring Boot and the disciplines — timeouts, retries, idempotency — that keep them reliable.

Synchronous vs asynchronous

SYNCHRONOUS                         ASYNCHRONOUS
orders ──HTTP request──► inventory  orders ──event──► [broker] ──► inventory
   ◄─────response────────┘            (fire-and-forget, decoupled)
AspectSynchronous (HTTP)Asynchronous (messaging)
CouplingTemporal — callee must be up nowDecoupled — broker buffers
LatencyImmediate responseEventual processing
ConsistencyEasy to reason aboutEventual consistency
Failure handlingCaller waits / retriesBroker retries, dead-letter
BackpressureHard (callee can be overwhelmed)Natural (queue absorbs load)
Best forReads, queries, request/replyState changes, fan-out, workflows

Tip: A useful default — use synchronous calls for queries (you need the answer now) and asynchronous events for commands that propagate state (other services react in their own time). This keeps write paths resilient.

Synchronous: OpenFeign

OpenFeign turns a Java interface into an HTTP client and integrates with discovery and load balancing automatically.

@FeignClient(name = "inventory-service")   // resolved + load-balanced via discovery
public interface InventoryClient {

    @GetMapping("/inventory/{sku}")
    InventoryResponse getStock(@PathVariable String sku);
}
@Service
@RequiredArgsConstructor
public class OrderService {
    private final InventoryClient inventory;

    public void reserve(String sku) {
        InventoryResponse stock = inventory.getStock(sku);   // a plain method call
        // ...
    }
}

Enable it once on a config class:

@SpringBootApplication
@EnableFeignClients
public class OrdersApplication { }

Synchronous: RestClient / WebClient

For more control, call services directly with a @LoadBalanced RestClient (blocking) or WebClient (reactive), addressing them by logical name:

InventoryResponse stock = inventoryClient.get()
        .uri("http://inventory-service/inventory/{sku}", sku)
        .retrieve()
        .body(InventoryResponse.class);

See Load Balancing for the @LoadBalanced setup.

Asynchronous: messaging

For state changes, publish an event and let interested services consume it on their own schedule. With RabbitMQ:

@Service
@RequiredArgsConstructor
public class OrderService {
    private final RabbitTemplate rabbit;

    @Transactional
    public Order place(OrderRequest req) {
        Order order = repository.save(Order.from(req));
        rabbit.convertAndSend("orders.exchange", "order.placed",
                new OrderPlaced(order.getId(), req.items()));
        return order;
    }
}
@Component
public class InventoryConsumer {
    @RabbitListener(queues = "inventory.order-placed")
    public void on(OrderPlaced event) {
        // reserve stock; failure here doesn't break the order flow
    }
}

This decouples orders from inventory’s availability and underpins the Saga Pattern. See Messaging with Spring for brokers and patterns.

Timeouts

A call without a timeout is a latent outage. Always bound both connect and read time.

# OpenFeign timeouts (per client or default)
spring:
  cloud:
    openfeign:
      client:
        config:
          inventory-service:
            connect-timeout: 1000
            read-timeout: 3000

Warning: The default read timeout for many HTTP clients is infinite. One stuck dependency can exhaust the caller’s threads. Set explicit, aggressive timeouts and pair them with a circuit breaker.

Retries and idempotency

Networks fail transiently, so retries are essential — but only safe for idempotent operations. GET, PUT, and DELETE are naturally idempotent; POST usually is not.

For non-idempotent commands, make them idempotent with an idempotency key the receiver deduplicates on:

@PostMapping("/payments")
public PaymentResponse charge(@RequestHeader("Idempotency-Key") String key,
                              @RequestBody ChargeRequest req) {
    return payments.findByKey(key)              // already processed? return prior result
            .orElseGet(() -> payments.charge(key, req));
}
OperationIdempotent?Safe to retry blindly?
GET /orders/42YesYes
PUT /orders/42 (full replace)YesYes
DELETE /orders/42YesYes
POST /paymentsNoOnly with an idempotency key

Combine retries, timeouts, and circuit breaking with Resilience4j — see Circuit Breaker (Resilience4j). And to debug a request as it crosses services, you need Distributed Tracing.

Last updated June 13, 2026
Was this helpful?