Skip to content
Chapter 03 · Microservices

FeignClient: Declarative HTTP Calls ​

RestTemplate and RestClient both make you write how to call product-service: build the request, send it, convert the response. Feign is a declarative HTTP client: you write what to call, as an annotated interface, and the framework writes the how.

Feign was created by Netflix. In Spring Boot it comes through the Spring Cloud OpenFeign library. Spring Cloud is a set of libraries for building distributed microservices, all designed to work together:

  • service discovery
  • client-side load balancing
  • circuit breakers and other resilience patterns
  • API gateway
  • distributed tracing
  • centralized configuration

Because Feign plugs into all of them, it is the client the rest of this book uses for service-to-service calls.

Step 1: add the dependency ​

In order-service's pom.xml:

xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

There is no version. Later chapters add more Spring Cloud libraries (discovery, load balancer...), and their versions must all be compatible with each other and with Spring Boot. Instead of managing that by hand, import the Spring Cloud BOM (bill of materials), which pins a compatible version for every Spring Cloud library:

xml
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2023.0.3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

The BOM version is a release train that must match your Spring Boot version: 2023.0.x goes with Boot 3.2 and 3.3 (use 2023.0.3 or later for Boot 3.3), 2024.0.x with Boot 3.4. start.spring.io picks the right one when you add any Spring Cloud dependency.

Step 2: declare the client ​

In order-service, write an interface describing the endpoint of product-service:

java
@FeignClient(name = "product-service", url = "${product-service.url}")
public interface ProductClient {

    @GetMapping("/products/{id}")
    String getProductById(@PathVariable("id") String id);
}

It reads exactly like a controller method, because it mirrors one: product-service has @GetMapping("/products/{id}"), so the client declares the same mapping.

  • @FeignClient marks the interface for Feign to implement.
  • name is a name you choose for this client. It keys per-client configuration (Step 6) and, once service discovery arrives, names the service to call.
  • url is the base URL: host and port. It is joined with the method's path to make the full URL.

Keep the base URL in application.properties so it is easy to change. The property name is your choice:

properties
server.port=8081
product-service.url=http://localhost:8082

Step 3: use it ​

java
@RestController
@RequestMapping("/orders")
public class OrderController {

    @Autowired
    ProductClient productClient;

    @GetMapping("/{id}")
    public ResponseEntity<String> getOrder(@PathVariable String id) {

        String response = productClient.getProductById(id);
        System.out.println("Response from Product api call is: " + response);

        return ResponseEntity.ok("order call successful");
    }
}

Step 4: enable Feign ​

Feign does nothing until you enable it on the application class:

java
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

@EnableFeignClients tells Spring Boot to scan for interfaces annotated with @FeignClient. Without it, the interface is never implemented and the injection fails.

GET /orders/1 now returns order call successful and prints Response from Product api call is: Product fetched with id: 1. Four steps, no HTTP code.

How the declarative call works ​

ProductClient is an interface with no implementation, yet Spring injects an object for it. Something must implement it at runtime. At startup, for each @FeignClient interface, Feign (in its ReflectiveFeign.newInstance method) does three things.

1. Build a MethodHandler per method. It parses each method's signature and annotations into a MethodHandler object holding everything needed to make the call:

FieldComes from
target URLthe url attribute + the mapping's path
HTTP method@GetMapping, @PutMapping...
headers@RequestHeader parameters
HTTP clientby default Feign's Client.Default, built on HttpURLConnection (configurable)
encoderturns a Java object into a request body (default: Spring's message converters, Jackson for JSON)
decoderturns a response body into a Java object
error decoderturns a non-2xx response into an exception
retryer, logger, timeoutsconfiguration

The results go into a map: Method -> MethodHandler.

2. Create an InvocationHandler that holds that map. Its invoke method is the bridge between the proxy and the method handlers.

3. Create a dynamic proxy. Java's Proxy.newProxyInstance generates, at runtime, a class that implements ProductClient. That object is the bean Spring injects. Printing productClient.getClass().getName() shows something like jdk.proxy2.$Proxy67.

Conceptually, the generated class behaves like this (no such source file exists; the JDK generates the class):

java
class ProductClientProxy implements ProductClient {

    private final InvocationHandler handler;

    @Override
    public String getProductById(String id) {
        Method method = ProductClient.class.getMethod("getProductById", String.class);
        return (String) handler.invoke(this, method, new Object[]{ id });
    }
}

So a call flows like this:

The MethodHandler.invoke method is where the real work happens:

java
public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);   // encoder runs here
    Options options = findOptions(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
        try {
            return executeAndDecode(template, options);   // send; decoder or error decoder
        } catch (RetryableException e) {
            retryer.continueOrPropagate(e);               // wait and loop, or rethrow
        }
    }
}

Keep this loop in mind; the encoder, decoder, error decoder and retryer below all plug into it.

Mapping parameters ​

Every Spring MVC annotation you know from controllers works on a Feign method:

java
@FeignClient(name = "product-service", url = "${product-service.url}")
public interface ProductClient {

    @GetMapping("/products/{id}")
    String getProductById(@PathVariable("id") String id);

    @PutMapping(value = "/products/update/{id}", consumes = "application/json")
    Product updateProduct(
            @PathVariable("id") String id,              // fills {id}
            @RequestParam("sendMail") boolean sendMail, // adds ?sendMail=true
            @RequestHeader("X-Request-ID") String requestId,
            @RequestBody Product updatedProduct);       // JSON body
}

The matching endpoint in product-service:

java
@PutMapping("/update/{id}")
public ResponseEntity<Product> updateProduct(
        @PathVariable String id,
        @RequestBody Product product,
        @RequestParam("sendMail") boolean sendMail,
        @RequestHeader("X-Request-ID") String requestId) {
    ...
}

The parameter order does not have to match between the two; the annotations decide where each value goes. Calling updateProduct("5", true, "abc", product) sends PUT /products/update/5?sendMail=true with header X-Request-ID: abc and the product as JSON.

On the client, give @PathVariable, @RequestParam and @RequestHeader their names explicitly, as above.

Step 5: per-client configuration ​

An order-service may call several services: products, users, payments. Each may need its own encoder, error handling, retries or timeouts. The configuration attribute attaches a configuration class to one client:

java
@FeignClient(name = "product-service",
             url = "${product-service.url}",
             configuration = ProductClientConfig.class)
public interface ProductClient { ... }
java
public class ProductClientConfig {      // note: no @Configuration

    @Bean
    public ErrorDecoder errorDecoder() { ... }
}

Feign creates a small Spring context for each client, and the beans of this class go only into the product-service client's context.

Leave @Configuration off this class. If it is annotated and sits inside the component-scan package, its beans are registered in the main application context, and every Feign client picks them up, which defeats the purpose.

Encoder and decoder ​

  • Encoder: converts a Java object into the request body (say JSON).
  • Decoder: converts the response body (say JSON) into a Java object.

They are Feign's version of serialization and deserialization. The defaults (SpringEncoder, SpringDecoder) use Spring's message converters, so JSON goes through Jackson, the same as in a controller. You rarely need your own, but you can:

java
public class MyCustomProductClientEncoder implements Encoder {

    @Override
    public void encode(Object object, Type bodyType, RequestTemplate template) {
        try {
            template.body(new ObjectMapper().writeValueAsString(object));
        } catch (Exception e) {
            throw new EncodeException("Unable to encode object");
        }
    }
}
java
public class MyCustomProductClientDecoder implements Decoder {

    @Override
    public Object decode(Response response, Type type) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        InputStream body = response.body().asInputStream();
        return mapper.readValue(body, mapper.constructType(type));
    }
}

type is the method's return type. Register both in the client's configuration class:

java
public class ProductClientConfig {

    @Bean
    public Encoder encoder() { return new MyCustomProductClientEncoder(); }

    @Bean
    public Decoder decoder() { return new MyCustomProductClientDecoder(); }
}

ErrorDecoder ​

For a non-2xx response, executeAndDecode calls the error decoder. The default, ErrorDecoder.Default, returns a FeignException subclass with the status, request and body:

text
feign.FeignException$BadRequest: [400] during [GET] to [http://localhost:8082/products/bad]
  [ProductClient#getProductById(String)]: [bad id]

To throw your own exceptions, implement ErrorDecoder and delegate anything you do not handle to the default:

java
public class MyCustomProductClientErrorDecoder implements ErrorDecoder {

    private final ErrorDecoder defaultErrorDecoder = new ErrorDecoder.Default();

    @Override
    public Exception decode(String methodKey, Response response) {
        HttpStatus status = HttpStatus.valueOf(response.status());

        if (status.is4xxClientError()) {
            return new MyCustomBadRequestException("Client Error");
        } else if (status.is5xxServerError()) {
            return new MyCustomServerException("Server Error");
        }
        return defaultErrorDecoder.decode(methodKey, response);
    }
}
java
public class ProductClientConfig {

    @Bean
    public ErrorDecoder errorDecoder() { return new MyCustomProductClientErrorDecoder(); }
}

Note that decode returns the exception; Feign throws it.

Retryer ​

Look again at the loop in MethodHandler.invoke. When a call fails with a RetryableException, the retryer's continueOrPropagate either sleeps (and the loop tries again) or rethrows.

  • A RetryableException comes from I/O failures: connection refused, connect timeout, read timeout. (The default error decoder also makes one for a 503 that carries a Retry-After header.)
  • Ordinary 4xx and 5xx responses are not retried; they go straight to the error decoder.

The default in Spring Cloud OpenFeign is Retryer.NEVER_RETRY: one attempt, no retries. Plain Feign (without Spring Cloud) defaults to Retryer.Default, which is why you will see both described.

To enable retries, register Retryer.Default:

java
public class ProductClientConfig {

    @Bean
    public Retryer retryer() {
        return new Retryer.Default();     // period 100 ms, maxPeriod 1 s, maxAttempts 5
    }
}

Retryer.Default makes 5 attempts in total: the first call plus 4 retries. The wait grows by a factor of 1.5 each time, capped at maxPeriod. Against a port nothing listens on, the attempts land at:

text
attempt 1     0 ms
attempt 2   150 ms later
attempt 3   225 ms later
attempt 4   337 ms later
attempt 5   506 ms later      -> RetryableException thrown (about 1.2 s in total)

To change only the numbers, extend Retryer.Default:

java
public class MyCustomRetryer extends Retryer.Default {

    public MyCustomRetryer() {
        super(200, 1000, 4);     // period ms, maxPeriod ms, maxAttempts
    }
}

For full control, implement Retryer yourself:

java
public class MyCustomRetryer implements Retryer {

    private int attempt = 1;
    private final int maxAttempts = 5;

    @Override
    public void continueOrPropagate(RetryableException e) {
        if (attempt >= maxAttempts) {
            throw e;                         // give up
        }
        attempt++;
        try {
            Thread.sleep(100);               // fixed wait
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            throw e;
        }
    }

    @Override
    public Retryer clone() {
        return new MyCustomRetryer();        // fresh counter for every call
    }
}

clone() matters: invoke calls this.retryer.clone() for each request, so every call starts with its own attempt counter.

Chapter 8 covers retries properly, with Resilience4j.

Step 6: timeouts per client ​

The name in @FeignClient is the key for property-based configuration. In Spring Cloud OpenFeign 4.x (Spring Boot 3), the prefix is spring.cloud.openfeign.client.config:

properties
# only the product-service client
spring.cloud.openfeign.client.config.product-service.connect-timeout=3000
spring.cloud.openfeign.client.config.product-service.read-timeout=5000

# every Feign client
spring.cloud.openfeign.client.config.default.connect-timeout=3000
spring.cloud.openfeign.client.config.default.read-timeout=5000

The older prefix feign.client.config... from Spring Cloud 2022 and earlier is silently ignored now. With a 2-second endpoint and a 500 ms read timeout under each prefix, the old one waits the full 2 seconds, and the new one fails after 500 ms with RetryableException: Read timed out.

The same name-keyed properties can also set the error decoder, retryer, logger level and more, as an alternative to a configuration class.

Next: The client still contains localhost:8082. Real services run on many machines whose addresses change. Chapter 4 introduces service discovery with Eureka, so services find each other by name.

Check yourself ​

  1. What does "declarative" mean for Feign?
  2. Why is the Spring Cloud BOM imported, and what must its version match?
  3. What does @EnableFeignClients do?
  4. Who implements a @FeignClient interface, and how?
  5. What does a MethodHandler hold, and when is it built?
  6. What role does the InvocationHandler play?
  7. Must the parameter order of a Feign method match the server's controller method?
  8. Why should a client's configuration class not be annotated @Configuration?
  9. What does ErrorDecoder.Default produce for a 400?
  10. Which failures does Feign retry, and which does it not?
  11. How many attempts does Spring Cloud OpenFeign make by default? And with Retryer.Default?
  12. Why must a custom Retryer implement clone()?
  13. Which property sets the read timeout of only the product-service client in Spring Boot 3?
Answers
  1. You declare what to call (an annotated interface) and the framework generates how to call it.
  2. It pins mutually compatible versions of all Spring Cloud libraries. Its release train must match the Spring Boot version (2023.0.x for Boot 3.2/3.3).
  3. It makes Spring Boot scan for @FeignClient interfaces and create their implementations.
  4. Feign, at startup, with a JDK dynamic proxy that implements the interface and forwards every call to an InvocationHandler.
  5. Everything needed for one method's HTTP call: target URL, HTTP method, headers, client, encoder, decoder, error decoder, retryer, options. It is built at startup by parsing the method and its annotations.
  6. It is the bridge: it receives the proxy's call, looks up the method's MethodHandler in its map and calls its invoke.
  7. No; the annotations decide where each value goes.
  8. If it is component-scanned, its beans land in the main context and apply to every Feign client, not just the one that names it.
  9. A FeignException.BadRequest carrying the status, method, URL and response body.
  10. It retries RetryableExceptions: I/O failures such as connection refused or timeouts (and a 503 with Retry-After). It does not retry ordinary 4xx/5xx responses; those go to the error decoder.
  11. One (Retryer.NEVER_RETRY). With Retryer.Default, five: the first call plus four retries, waits growing ×1.5 from 150 ms, capped at 1 s.
  12. invoke clones the retryer for every request, so each call needs its own fresh state (attempt counter).
  13. spring.cloud.openfeign.client.config.product-service.read-timeout=...