Building Microservices with Spring Boot

Declarative Clients with OpenFeign

18 min Lesson 4 of 12

Declarative Clients with OpenFeign

In the previous lesson you built service-to-service calls with WebClient. It works, but it requires you to assemble URLs by hand, manage headers explicitly, and write imperative reactive chains. As your microservice landscape grows, that boilerplate compounds quickly. Spring Cloud OpenFeign solves this by letting you define an HTTP client as a plain Java interface — the same mental model you already use for Spring Data repositories — and letting the framework generate the implementation for you at startup.

What OpenFeign Is and How It Works

OpenFeign (originally Netflix Feign, now owned by the OpenFeign community) is a declarative HTTP client. You annotate an interface with @FeignClient and standard Spring MVC annotations (@GetMapping, @PostMapping, path variables, request params, request bodies). At startup, Spring Cloud scans for those interfaces and creates JDK dynamic proxies that translate every method call into a real HTTP request.

Declarative vs. imperative: With WebClient you write the how (build a request, add headers, subscribe). With Feign you write the what (call getOrder(id)), and the framework handles the rest. The result is dramatically less code and a much easier audit surface.

Adding the Dependency

Add the Spring Cloud OpenFeign starter to your service's pom.xml. You also need the Spring Cloud BOM in your dependency management block:

<!-- In <dependencyManagement> --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2023.0.2</version> <type>pom</type> <scope>import</scope> </dependency> <!-- In <dependencies> --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>

Enabling Feign in Your Application

Annotate your main class (or any @Configuration class) with @EnableFeignClients. Without this, Spring will never scan for @FeignClient interfaces.

package com.example.orderservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableFeignClients public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } }

Declaring Your First Feign Client

Suppose the Order Service needs to look up products from the Inventory Service. The Inventory Service exposes a GET /products/{id} endpoint. Here is the complete Feign client declaration:

package com.example.orderservice.client; import com.example.orderservice.dto.ProductResponse; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; @FeignClient(name = "inventory-service", url = "${services.inventory.url}") public interface InventoryClient { @GetMapping("/products/{id}") ProductResponse getProduct(@PathVariable("id") Long id); @GetMapping("/products") List<ProductResponse> listProducts(@RequestParam("category") String category); }

A few things to notice:

  • name is a logical name used for metrics and (when combined with a service registry) for load-balanced lookup.
  • url is the base URL, read here from application.yml. In a service-discovery environment (Eureka, Consul) you can omit url and Feign will resolve it from the registry by name.
  • The method signatures mirror the controller methods in the target service — same annotations, same types.

Injecting and Using the Client

Spring registers the generated proxy as a bean with the interface type. Inject it exactly as you would any other Spring bean:

package com.example.orderservice.service; import com.example.orderservice.client.InventoryClient; import com.example.orderservice.dto.ProductResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class OrderService { private final InventoryClient inventoryClient; public void placeOrder(Long productId, int quantity) { ProductResponse product = inventoryClient.getProduct(productId); // product is fully deserialized — no reactive chains, no manual parsing if (product.getStock() < quantity) { throw new IllegalStateException("Insufficient stock for: " + product.getName()); } // ... persist the order } }
Keep the client interface in a dedicated client package and co-locate its DTO classes there too. This draws a clear boundary: the client package owns everything the external service contract requires, making it easy to swap out or version later.

Sending a Request Body

POST and PUT requests work just as naturally. Annotate the body parameter with @RequestBody:

@FeignClient(name = "notification-service", url = "${services.notification.url}") public interface NotificationClient { @PostMapping("/notifications") NotificationResponse sendNotification(@RequestBody NotificationRequest request); }

Customising Request Headers

You often need to propagate headers — correlation IDs, authorization tokens, tenant identifiers — to downstream services. Feign provides two mechanisms:

1. Per-method header annotation — for fixed or caller-supplied header values:

@GetMapping(value = "/internal/orders/{id}", headers = "X-Internal-Key=secret-key") OrderResponse getOrderInternal(@PathVariable Long id);

2. Request interceptor — for headers you want added to every call made by a specific client (or globally):

package com.example.orderservice.config; import feign.RequestInterceptor; import feign.RequestTemplate; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; @Component public class CorrelationIdInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { var attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes != null) { String correlationId = attributes.getRequest() .getHeader("X-Correlation-ID"); if (correlationId != null) { template.header("X-Correlation-ID", correlationId); } } } }

Because this bean is a @Component it is applied globally — every Feign client in the application will forward the correlation ID. To scope it to a single client, declare it inside a @Configuration class and reference it via @FeignClient(configuration = MyConfig.class).

Do not blindly forward the Authorization header from incoming requests. If service A receives a user JWT and forwards it unchanged to service B, you have created a security hole: service B now accepts user-level tokens for what should be an internal, service-to-service call. Use a separate mechanism — a shared secret, mTLS, or a dedicated service account token — for internal communication, and never let user credentials leak between services.

Error Handling with ErrorDecoder

By default, any 4xx or 5xx response from the downstream service throws a generic FeignException. You can map HTTP error codes to domain exceptions with a custom ErrorDecoder:

package com.example.orderservice.config; import com.example.orderservice.exception.ProductNotFoundException; import feign.Response; import feign.codec.ErrorDecoder; import org.springframework.stereotype.Component; @Component public class InventoryErrorDecoder implements ErrorDecoder { private final ErrorDecoder defaultDecoder = new Default(); @Override public Exception decode(String methodKey, Response response) { return switch (response.status()) { case 404 -> new ProductNotFoundException( "Product not found — downstream: " + response.request().url()); case 503 -> new ServiceUnavailableException("Inventory service unavailable"); default -> defaultDecoder.decode(methodKey, response); }; } }

Register it in a Feign configuration class and reference it from the client annotation to scope it, or declare it as a @Component for global application.

Timeouts and Connection Settings

Feign's timeout defaults are dangerously high in some versions. Set explicit values in application.yml:

spring: cloud: openfeign: client: config: default: # applies to all clients connectTimeout: 2000 # ms to establish TCP connection readTimeout: 5000 # ms to wait for response body inventory-service: # overrides for this specific client readTimeout: 10000

OpenFeign vs. WebClient — Choosing the Right Tool

  • Use OpenFeign when you want concise, readable, synchronous-style client code and the calling thread can afford to block. It integrates seamlessly with Spring MVC (servlet-stack) applications and is the default choice for most microservice teams.
  • Use WebClient when your application is reactive (Spring WebFlux, non-blocking I/O) or when you need fine-grained control over streaming, partial responses, or back-pressure.
  • Mixing blocking Feign calls inside a reactive pipeline will stall event-loop threads and degrade the entire application — pick one model and stick to it per service.

Summary

OpenFeign turns inter-service HTTP calls into plain Java interface method invocations. You declare the contract, annotate the methods, inject the bean, and call it. Request interceptors handle cross-cutting header propagation; ErrorDecoder translates HTTP error codes into domain exceptions; YAML properties control timeouts. Combined, these pieces give you a clean, type-safe, auditable communication layer that scales gracefully as the number of services grows. In the next lesson you will add resilience patterns — retries, circuit breakers, and fallbacks — to protect these calls from downstream failures.