Spring BootInter-Service Communication
Spring Boot

Inter-Service Communication

Microservices communicate through synchronous HTTP calls or asynchronous messaging. Spring Boot provides RestClient and WebClient for HTTP, Spring Cloud OpenFeign for declarative REST clients, Spring Cloud Circuit Breaker (Resilience4j) for fault tolerance, and Spring Kafka or RabbitMQ for messaging. This entry covers all four patterns with retry, circuit breaking, timeout, and fallback strategies.

RestClient for Synchronous HTTP

RestClient is the modern Spring 6 HTTP client for synchronous service-to-service calls. Combine it with Spring Cloud LoadBalancer for service-name resolution and Micrometer for automatic tracing and metrics. Configure per-client timeouts, error handling, and base URLs.
Java
// ── Client configuration ──────────────────────────────────────────────
@Configuration
public class ServiceClientConfig {

    // ── Load-balanced builder (resolves service names via Eureka) ─────
    @Bean
    @LoadBalanced
    public RestClient.Builder loadBalancedBuilder() {
        return RestClient.builder();
    }

    // ── Inventory service client ──────────────────────────────────────
    @Bean
    public RestClient inventoryRestClient(
            @LoadBalanced RestClient.Builder builder) {
        return builder
            .baseUrl("http://inventory-service")
            .defaultHeader(HttpHeaders.CONTENT_TYPE,
                MediaType.APPLICATION_JSON_VALUE)
            .defaultHeader(HttpHeaders.ACCEPT,
                MediaType.APPLICATION_JSON_VALUE)
            .build();
    }

    // ── Payment service client ────────────────────────────────────────
    @Bean
    public RestClient paymentRestClient(
            @LoadBalanced RestClient.Builder builder) {
        return builder
            .baseUrl("http://payment-service")
            .requestInterceptor((request, body, execution) -> {
                // Propagate JWT to downstream services
                Authentication auth = SecurityContextHolder
                    .getContext().getAuthentication();
                if (auth != null &&
                        auth.getCredentials() instanceof String jwt) {
                    request.getHeaders()
                        .set(HttpHeaders.AUTHORIZATION,
                            "Bearer " + jwt);
                }
                return execution.execute(request, body);
            })
            .build();
    }
}

// ── Inventory client service ──────────────────────────────────────────
@Service
@RequiredArgsConstructor
@Slf4j
public class InventoryServiceClient {

    private final RestClient inventoryRestClient;

    public StockResponse checkStock(Long productId) {
        return inventoryRestClient.get()
            .uri("/api/v1/stock/{id}", productId)
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError,
                (req, res) -> {
                    throw new ServiceCallException(
                        "Inventory service returned: "
                        + res.getStatusCode());
                })
            .onStatus(HttpStatusCode::is5xxServerError,
                (req, res) -> {
                    throw new ServiceUnavailableException(
                        "Inventory service unavailable");
                })
            .body(StockResponse.class);
    }

    public ReservationResponse reserve(
            Long orderId, List<OrderItem> items) {
        return inventoryRestClient.post()
            .uri("/api/v1/reservations")
            .body(new ReservationRequest(orderId, items))
            .retrieve()
            .body(ReservationResponse.class);
    }
}

OpenFeign Declarative Clients

Spring Cloud OpenFeign generates HTTP client implementations from annotated interfaces. No implementation code needed — declare the interface, annotate with @FeignClient, and Spring creates a load-balanced, traced, and retryable implementation. Feign integrates with Resilience4j for circuit breaking.
XML
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

// ── Enable Feign clients ───────────────────────────────────────────────
@SpringBootApplication
@EnableFeignClients(basePackages = "com.myapp.client")
public class OrderServiceApplication { ... }

// ── Feign client interface ─────────────────────────────────────────────
@FeignClient(
    name      = "inventory-service",  // Eureka service name
    path      = "/api/v1",
    fallback  = InventoryClientFallback.class,
    configuration = FeignClientConfig.class
)
public interface InventoryFeignClient {

    @GetMapping("/stock/{productId}")
    StockResponse checkStock(
        @PathVariable Long productId);

    @PostMapping("/reservations")
    ReservationResponse reserve(
        @RequestBody ReservationRequest request);

    @DeleteMapping("/reservations/{reservationId}")
    void cancelReservation(
        @PathVariable String reservationId);

    @GetMapping("/stock")
    Page<StockResponse> findLowStock(
        @RequestParam int threshold,
        @SpringQueryMap Pageable pageable);
}

// ── Fallback ──────────────────────────────────────────────────────────
@Component
@Slf4j
public class InventoryClientFallback
        implements InventoryFeignClient {

    @Override
    public StockResponse checkStock(Long productId) {
        log.warn("Fallback: inventory check failed for {}",
            productId);
        return StockResponse.unavailable(productId);
    }

    @Override
    public ReservationResponse reserve(
            ReservationRequest request) {
        throw new ServiceUnavailableException(
            "Inventory service is currently unavailable");
    }

    @Override
    public void cancelReservation(String reservationId) {
        log.warn("Fallback: could not cancel reservation {}",
            reservationId);
    }

    @Override
    public Page<StockResponse> findLowStock(
            int threshold, Pageable pageable) {
        return Page.empty();
    }
}

// ── Feign client configuration ────────────────────────────────────────
@Configuration
public class FeignClientConfig {

    @Bean
    public Retryer retryer() {
        // Retry 3 times with 100ms initial interval, 1s max
        return new Retryer.Default(100, 1000, 3);
    }

    @Bean
    public ErrorDecoder errorDecoder() {
        return (methodKey, response) -> {
            if (response.status() == 404) {
                return new ResourceNotFoundException(
                    "Resource not found: " + methodKey);
            }
            if (response.status() >= 500) {
                return new RetryableException(
                    response.status(), "Server error",
                    Request.HttpMethod.GET, null,
                    response.request());
            }
            return new ServiceCallException(
                "Feign error: " + response.status());
        };
    }

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.BASIC;  // NONE, BASIC, HEADERS, FULL
    }
}

Circuit Breaker with Resilience4j

Circuit breakers prevent cascading failures. When a downstream service starts failing, the circuit opens and requests fail fast with a fallback instead of waiting for a timeout. Resilience4j integrates with Spring AOP through annotations and with Spring Cloud Circuit Breaker through a fluent API.
XML
<!-- pom.xml -->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

# ── application.yml — Resilience4j configuration ─────────────────────
resilience4j:
  circuitbreaker:
    instances:
      inventory-service:
        sliding-window-type:             count_based
        sliding-window-size:             10
        failure-rate-threshold:          50     # open at 50% failures
        wait-duration-in-open-state:     10s    # wait before half-open
        permitted-calls-in-half-open:    3
        slow-call-duration-threshold:    2s
        slow-call-rate-threshold:        50
        record-exceptions:
          - java.io.IOException
          - java.util.concurrent.TimeoutException
          - com.myapp.exception.ServiceUnavailableException
        ignore-exceptions:
          - com.myapp.exception.ValidationException

  retry:
    instances:
      inventory-service:
        max-attempts:            3
        wait-duration:           200ms
        retry-exceptions:
          - java.io.IOException
          - com.myapp.exception.ServiceUnavailableException

  timelimiter:
    instances:
      inventory-service:
        timeout-duration:        3s
        cancel-running-future:   true

  bulkhead:
    instances:
      inventory-service:
        max-concurrent-calls:    10
        max-wait-duration:       100ms

// ── Service with circuit breaker annotations ──────────────────────────
@Service
@RequiredArgsConstructor
@Slf4j
public class InventoryServiceClient {

    private final RestClient inventoryRestClient;

    @CircuitBreaker(name = "inventory-service",
                    fallbackMethod = "checkStockFallback")
    @Retry(name = "inventory-service")
    @TimeLimiter(name = "inventory-service")
    @Bulkhead(name = "inventory-service",
              type = Bulkhead.Type.SEMAPHORE)
    public CompletableFuture<StockResponse> checkStock(
            Long productId) {
        return CompletableFuture.supplyAsync(() ->
            inventoryRestClient.get()
                .uri("/api/v1/stock/{id}", productId)
                .retrieve()
                .body(StockResponse.class));
    }

    // Fallback — called when circuit is open or call fails
    public CompletableFuture<StockResponse> checkStockFallback(
            Long productId, Exception ex) {
        log.warn("Circuit breaker fallback for product {}: {}",
            productId, ex.getMessage());
        // Return a degraded response — assume in stock
        return CompletableFuture.completedFuture(
            StockResponse.assumed(productId, true));
    }

    // ── Monitor circuit state ──────────────────────────────────────────
    @Autowired
    private CircuitBreakerRegistry registry;

    public CircuitBreaker.State getCircuitState() {
        return registry.circuitBreaker("inventory-service")
            .getState();
    }
}

WebClient for Reactive Inter-Service Calls

WebClient provides non-blocking reactive HTTP calls. Use it in Spring WebFlux applications or where reactive pipelines benefit from non-blocking I/O. Combine with Reactor's retry, timeout, and fallback operators for resilience without blocking threads.
Java
@Configuration
public class ReactiveClientConfig {

    @Bean
    @LoadBalanced
    public WebClient.Builder loadBalancedWebClientBuilder() {
        return WebClient.builder();
    }

    @Bean
    public WebClient inventoryWebClient(
            @LoadBalanced WebClient.Builder builder) {
        return builder
            .baseUrl("http://inventory-service")
            .defaultHeader(HttpHeaders.CONTENT_TYPE,
                MediaType.APPLICATION_JSON_VALUE)
            .filter(ExchangeFilterFunctions
                .basicAuthentication("service", "secret"))
            .build();
    }
}

// ── Reactive service client ────────────────────────────────────────────
@Service
@RequiredArgsConstructor
@Slf4j
public class ReactiveInventoryClient {

    private final WebClient inventoryWebClient;

    public Mono<StockResponse> checkStock(Long productId) {
        return inventoryWebClient.get()
            .uri("/api/v1/stock/{id}", productId)
            .retrieve()
            .onStatus(HttpStatusCode::is5xxServerError,
                res -> res.bodyToMono(String.class)
                    .flatMap(body ->
                        Mono.error(new ServiceUnavailableException(
                            "Inventory error: " + body))))
            .bodyToMono(StockResponse.class)
            // ── Retry up to 3 times with exponential backoff ──────────
            .retryWhen(Retry.backoff(3, Duration.ofMillis(200))
                .filter(ex -> ex instanceof IOException
                    || ex instanceof ServiceUnavailableException)
                .onRetryExhaustedThrow((spec, signal) ->
                    new ServiceUnavailableException(
                        "Inventory service exhausted retries")))
            // ── Timeout ───────────────────────────────────────────────
            .timeout(Duration.ofSeconds(3))
            // ── Fallback ──────────────────────────────────────────────
            .onErrorResume(ex -> {
                log.warn("Inventory check failed for {}: {}",
                    productId, ex.getMessage());
                return Mono.just(
                    StockResponse.assumed(productId, true));
            });
    }

    // ── Parallel calls to multiple services ───────────────────────────
    public Mono<OrderContext> loadOrderContext(Long orderId) {
        Mono<StockResponse>   stock    =
            checkStock(orderId).subscribeOn(Schedulers.boundedElastic());
        Mono<PricingResponse> pricing  =
            checkPricing(orderId).subscribeOn(Schedulers.boundedElastic());
        Mono<UserResponse>    user     =
            loadUser(orderId).subscribeOn(Schedulers.boundedElastic());

        // Execute all three in parallel, combine results
        return Mono.zip(stock, pricing, user)
            .map(tuple -> new OrderContext(
                tuple.getT1(),
                tuple.getT2(),
                tuple.getT3()));
    }
}

Asynchronous Messaging with Kafka

Asynchronous messaging decouples services in time — the producer does not wait for the consumer. Use Kafka for high-throughput event streaming between services. Spring Kafka provides @KafkaListener for consuming events and KafkaTemplate for producing them, with automatic trace context propagation.
XML
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

# ── application.yml ────────────────────────────────────────────────────
spring:
  kafka:
    bootstrap-servers: ${KAFKA_BROKERS:localhost:9092}
    producer:
      key-serializer: >
        org.apache.kafka.common.serialization.StringSerializer
      value-serializer: >
        org.springframework.kafka.support.serializer.JsonSerializer
      acks: all
      retries: 3
      properties:
        enable.idempotence: true
        max.in.flight.requests.per.connection: 1

    consumer:
      group-id: ${spring.application.name}
      key-deserializer: >
        org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: >
        org.springframework.kafka.support.serializer.JsonDeserializer
      auto-offset-reset: earliest
      enable-auto-commit: false
      properties:
        spring.json.trusted.packages: "com.myapp.events"

    listener:
      ack-mode: manual_immediate   # manual ack for exactly-once

// ── Event records ─────────────────────────────────────────────────────
public record OrderPlacedEvent(
    Long        orderId,
    Long        userId,
    BigDecimal  total,
    List<Long>  productIds,
    Instant     occurredAt
) {}

public record InventoryReservedEvent(
    Long    orderId,
    String  reservationId,
    boolean success,
    String  failureReason,
    Instant occurredAt
) {}

// ── Producer ──────────────────────────────────────────────────────────
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderEventProducer {

    private final KafkaTemplate<String, Object> kafkaTemplate;

    public void publishOrderPlaced(Order order) {
        OrderPlacedEvent event = new OrderPlacedEvent(
            order.getId(), order.getUserId(),
            order.getTotal(), order.getProductIds(),
            Instant.now());

        kafkaTemplate.send("orders.placed",
                String.valueOf(order.getId()), event)
            .whenComplete((result, ex) -> {
                if (ex != null) {
                    log.error("Failed to publish OrderPlaced " +
                        "for order {}", order.getId(), ex);
                } else {
                    log.info("Published OrderPlaced: order={} " +
                        "partition={} offset={}",
                        order.getId(),
                        result.getRecordMetadata().partition(),
                        result.getRecordMetadata().offset());
                }
            });
    }
}

// ── Consumer ──────────────────────────────────────────────────────────
@Component
@RequiredArgsConstructor
@Slf4j
public class OrderEventConsumer {

    private final InventoryService inventoryService;

    @KafkaListener(
        topics   = "orders.placed",
        groupId  = "inventory-service",
        containerFactory = "kafkaListenerContainerFactory"
    )
    public void onOrderPlaced(
            ConsumerRecord<String, OrderPlacedEvent> record,
            Acknowledgment ack) {

        OrderPlacedEvent event = record.value();
        log.info("Processing order {} from partition {} offset {}",
            event.orderId(),
            record.partition(),
            record.offset());

        try {
            inventoryService.reserveForOrder(event);
            ack.acknowledge();      // commit offset after success
        } catch (RetryableException ex) {
            log.warn("Retryable error processing order {}",
                event.orderId(), ex);
            // Do not ack — Kafka will redeliver
        } catch (Exception ex) {
            log.error("Non-retryable error — sending to DLQ",
                ex);
            ack.acknowledge();      // ack to skip, DLQ handles it
        }
    }

    // ── Dead letter queue consumer ────────────────────────────────────
    @KafkaListener(topics = "orders.placed.DLT",
                   groupId = "inventory-service-dlq")
    public void onOrderPlacedDlq(
            ConsumerRecord<String, OrderPlacedEvent> record) {
        log.error("DLQ: failed order event orderId={}",
            record.value().orderId());
        // Alert, store for manual review, etc.
    }
}

Saga Pattern for Distributed Transactions

Distributed transactions across microservices cannot use JTA when services have separate databases. The Saga pattern coordinates a multi-step business transaction as a sequence of local transactions with compensating actions for rollback. Use the choreography approach (events) for loose coupling or orchestration (central coordinator) for visibility.
Java
// ── Choreography-based saga ────────────────────────────────────────────
// Order Service → publishes OrderPlaced
// Inventory Service → reserves stock → publishes InventoryReserved
// Payment Service → charges card → publishes PaymentProcessed
// Order Service → confirms order → publishes OrderConfirmed
// If any step fails → publishes failure event
// → compensating transactions roll back previous steps

// ── Order service — saga orchestrator state ───────────────────────────
@Entity
@Table(name = "order_sagas")
@Getter @Setter @NoArgsConstructor
public class OrderSaga {

    @Id private Long orderId;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 30)
    private SagaState state = SagaState.STARTED;

    @Column(name = "reservation_id")
    private String reservationId;

    @Column(name = "payment_id")
    private String paymentId;

    @Column(name = "updated_at")
    private Instant updatedAt;
}

public enum SagaState {
    STARTED,
    INVENTORY_RESERVED,
    PAYMENT_PROCESSED,
    COMPLETED,
    INVENTORY_FAILED,
    PAYMENT_FAILED,
    COMPENSATING,
    COMPENSATED
}

// ── Order service — listening for saga events ─────────────────────────
@Component
@RequiredArgsConstructor
@Slf4j
public class OrderSagaManager {

    private final OrderSagaRepository   sagaRepo;
    private final OrderRepository       orderRepo;
    private final OrderEventProducer    producer;
    private final InventoryServiceClient inventoryClient;

    @KafkaListener(topics = "inventory.reserved")
    @Transactional
    public void onInventoryReserved(
            InventoryReservedEvent event,
            Acknowledgment ack) {
        OrderSaga saga = sagaRepo.findById(event.orderId())
            .orElseThrow();

        if (event.success()) {
            saga.setReservationId(event.reservationId());
            saga.setState(SagaState.INVENTORY_RESERVED);
            sagaRepo.save(saga);
            // Trigger next step — payment
            producer.publishPaymentRequested(saga);
        } else {
            // Compensate — cancel the order
            saga.setState(SagaState.INVENTORY_FAILED);
            sagaRepo.save(saga);
            orderRepo.findById(event.orderId())
                .ifPresent(o -> {
                    o.setStatus(OrderStatus.CANCELLED);
                    o.setFailureReason(event.failureReason());
                    orderRepo.save(o);
                });
            producer.publishOrderCancelled(saga);
        }
        ack.acknowledge();
    }

    @KafkaListener(topics = "payment.processed")
    @Transactional
    public void onPaymentProcessed(
            PaymentProcessedEvent event,
            Acknowledgment ack) {
        OrderSaga saga = sagaRepo.findById(event.orderId())
            .orElseThrow();

        if (event.success()) {
            saga.setPaymentId(event.paymentId());
            saga.setState(SagaState.COMPLETED);
            sagaRepo.save(saga);
            // Confirm the order
            orderRepo.findById(event.orderId())
                .ifPresent(o -> {
                    o.setStatus(OrderStatus.CONFIRMED);
                    orderRepo.save(o);
                });
        } else {
            // Compensate — release inventory reservation
            saga.setState(SagaState.COMPENSATING);
            sagaRepo.save(saga);
            inventoryClient.cancelReservation(
                saga.getReservationId());
        }
        ack.acknowledge();
    }
}