Inter-Service Communication with OpenFeign and RestClient

Services call each other over HTTP. You can use raw RestClient with a URL, or you can use OpenFeign — a declarative HTTP client that turns an interface into a fully functional HTTP client. This article covers both.

OpenFeign: Declarative HTTP Client

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication { }

Defining a Feign Client

@FeignClient(
    name = "inventory-service",    // service name — resolved via Eureka
    path = "/api/inventory"
)
public interface InventoryClient {

    @GetMapping("/products/{productId}/availability")
    InventoryResponse checkAvailability(
        @PathVariable UUID productId,
        @RequestParam int quantity
    );

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

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

    @GetMapping("/products")
    Page<ProductResponse> findProducts(
        @RequestParam String category,
        @SpringQueryMap Pageable pageable   // Pageable as query params
    );
}

Using it — just inject and call:

@Service
@RequiredArgsConstructor
public class OrderService {

    private final InventoryClient inventoryClient;

    public Order createOrder(CreateOrderRequest request) {
        // Calls http://inventory-service/api/inventory/products/{id}/availability
        InventoryResponse stock = inventoryClient.checkAvailability(
            request.productId(), request.quantity());

        if (!stock.available()) {
            throw new InsufficientStockException(request.productId());
        }

        // Reserve stock
        ReservationResponse reservation = inventoryClient.reserve(
            new ReservationRequest(request.productId(), request.quantity()));

        return buildOrder(request, reservation.id());
    }
}

No RestClient boilerplate, no URL construction, no header management.

Feign with Resilience4j

@FeignClient(
    name = "payment-service",
    path = "/api/payments",
    fallbackFactory = PaymentClientFallbackFactory.class
)
public interface PaymentClient {

    @PostMapping("/charge")
    @CircuitBreaker(name = "payment-service")
    PaymentResult charge(@RequestBody ChargeRequest request);
}

@Component
public class PaymentClientFallbackFactory implements FallbackFactory<PaymentClient> {

    @Override
    public PaymentClient create(Throwable cause) {
        return new PaymentClient() {
            @Override
            public PaymentResult charge(ChargeRequest request) {
                log.warn("Payment service unavailable: {}", cause.getMessage());
                return PaymentResult.pending(request.orderId());
            }
        };
    }
}

Enable Feign + Resilience4j integration:

spring:
  cloud:
    openfeign:
      circuitbreaker:
        enabled: true

Request Interceptors

Add authentication headers to all Feign calls:

@Component
public class ServiceAuthInterceptor implements RequestInterceptor {

    private final TokenProvider tokenProvider;

    @Override
    public void apply(RequestTemplate template) {
        // Add service-to-service auth token
        template.header("Authorization", "Bearer " + tokenProvider.getServiceToken());
        template.header("X-Service-Name", "order-service");
    }
}

Per-client interceptor:

@FeignClient(name = "payment-service", configuration = PaymentFeignConfig.class)
public interface PaymentClient { ... }

@Configuration
public class PaymentFeignConfig {

    @Bean
    public RequestInterceptor paymentAuthInterceptor() {
        return template -> template.header("X-API-Key", "${payment.api-key}");
    }

    @Bean
    public Retryer retryer() {
        return new Retryer.Default(100, 1000, 3);
    }
}

Error Handling

@Component
public class FeignErrorDecoder implements ErrorDecoder {

    private final ErrorDecoder defaultDecoder = new Default();

    @Override
    public Exception decode(String methodKey, Response response) {
        return switch (response.status()) {
            case 404 -> new ResourceNotFoundException("Resource not found: " + methodKey);
            case 409 -> new ConflictException("Conflict: " + methodKey);
            case 503 -> new ServiceUnavailableException(methodKey);
            default -> defaultDecoder.decode(methodKey, response);
        };
    }
}

Register globally or per-client in a @Configuration class.

Spring RestClient (No Feign)

For Spring Boot 3.2+, RestClient is the modern synchronous HTTP client — simpler than WebClient, more capable than RestTemplate:

@Configuration
public class RestClientConfig {

    @Bean
    @LoadBalanced    // enables service-name resolution via Eureka
    RestClient.Builder restClientBuilder() {
        return RestClient.builder()
            .defaultHeader("Content-Type", "application/json")
            .defaultHeader("X-Service-Name", "order-service");
    }
}
@Service
@RequiredArgsConstructor
public class CustomerClient {

    private final RestClient.Builder restClientBuilder;

    public CustomerResponse findCustomer(UUID customerId) {
        return restClientBuilder.build()
            .get()
            .uri("http://customer-service/api/customers/{id}", customerId)
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
                if (response.getStatusCode() == HttpStatus.NOT_FOUND) {
                    throw new CustomerNotFoundException(customerId);
                }
            })
            .body(CustomerResponse.class);
    }

    public List<CustomerResponse> findCustomers(List<UUID> customerIds) {
        return restClientBuilder.build()
            .post()
            .uri("http://customer-service/api/customers/batch")
            .body(new BatchRequest(customerIds))
            .retrieve()
            .body(new ParameterizedTypeReference<List<CustomerResponse>>() {});
    }
}

Wrapping RestClient in a Service

Encapsulate all calls to a service in one class:

@Service
@RequiredArgsConstructor
@Slf4j
public class InventoryServiceClient {

    private final RestClient.Builder restClientBuilder;

    @CircuitBreaker(name = "inventory-service", fallbackMethod = "defaultAvailability")
    @Retry(name = "inventory-service")
    public InventoryStatus checkAvailability(UUID productId, int quantity) {
        return restClientBuilder.build()
            .get()
            .uri("http://inventory-service/api/inventory/{productId}?quantity={qty}",
                productId, quantity)
            .retrieve()
            .body(InventoryStatus.class);
    }

    public InventoryStatus defaultAvailability(UUID productId, int quantity, Throwable ex) {
        log.error("Inventory service unavailable for product {}: {}", productId, ex.getMessage());
        return InventoryStatus.unknown(productId);
    }
}

Feign vs RestClient

OpenFeignRestClient
StyleDeclarative (interface)Imperative (builder)
BoilerplateMinimalModerate
Load balancinglb:// via name in @FeignClient@LoadBalanced builder
FallbackFallbackFactoryManual try/catch
Error handlingErrorDecoder.onStatus() handlers
Testing@MockBean the interfaceMock the whole client
AsyncNot nativeRestAsyncClient
Spring Boot integrationSpring Cloud requiredSpring Framework only

Use Feign when you want clean, minimal boilerplate and are already on Spring Cloud. Use RestClient when you want direct control or prefer to minimize Spring Cloud dependencies.

Testing Feign Clients

@SpringBootTest
@AutoConfigureWireMock(port = 0)    // starts WireMock on a random port
class InventoryClientTest {

    @Autowired InventoryClient inventoryClient;

    @Test
    void checksAvailability() {
        // Stub the inventory service
        stubFor(get(urlEqualTo("/api/inventory/products/" + PRODUCT_ID + "/availability?quantity=2"))
            .willReturn(aResponse()
                .withHeader("Content-Type", "application/json")
                .withBody("""
                    {"available": true, "quantity": 100}
                    """)));

        InventoryResponse response = inventoryClient.checkAvailability(PRODUCT_ID, 2);

        assertThat(response.available()).isTrue();
        assertThat(response.quantity()).isEqualTo(100);
    }

    @Test
    void handlesServiceUnavailable() {
        stubFor(get(anyUrl())
            .willReturn(serviceUnavailable()));

        assertThatThrownBy(() -> inventoryClient.checkAvailability(PRODUCT_ID, 2))
            .isInstanceOf(ServiceUnavailableException.class);
    }
}

Or use @MockBean for unit tests (no real HTTP):

@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired MockMvc mockMvc;
    @MockBean InventoryClient inventoryClient;    // Feign client as MockBean

    @Test
    @WithMockUser
    void createsOrderWhenStockAvailable() throws Exception {
        when(inventoryClient.checkAvailability(any(), anyInt()))
            .thenReturn(new InventoryResponse(true, 50));

        mockMvc.perform(post("/api/orders")
                .contentType(APPLICATION_JSON)
                .content("""{"productId": "...", "quantity": 2}"""))
            .andExpect(status().isCreated());
    }
}

What You’ve Learned

  • Feign turns an interface into a fully functional HTTP client — annotate methods like Spring MVC
  • @FeignClient(name = "service-name") resolves instances via Eureka with lb:// load balancing
  • FallbackFactory provides fallback implementations when the service is unavailable
  • RequestInterceptor adds headers to every Feign call — use for service-to-service auth
  • RestClient with @LoadBalanced is the modern alternative — more control, less magic
  • Wrap Feign clients or RestClient calls in service-layer classes that apply Resilience4j patterns
  • Test with WireMock for HTTP-level tests; @MockBean the Feign interface for unit tests

This completes Part 9: Microservices. You have the full toolkit for building resilient, observable microservices with Spring Boot and Spring Cloud.


Next: Part 10 — Containers and Cloud starts with Article 52: Dockerizing Spring Boot Applications.