Skip to content

FeignClient in Spring Boot Microservices | Synchronous Communication via Declarative HTTP Client

The Delivery Order Form Analogy

When a restaurant places an order with a food distributor, the chef fills in a form: "10 kg of tomatoes, delivered by Tuesday." The chef does not need to know which delivery truck will be used, which route it will take, or how the distributor's warehouse is organised. The form (the what) is handed to the distributor, who figures out the how entirely on their own.

FeignClient works exactly like that form. You declare an interface that says "I want to call this endpoint with these parameters and get back this type." The FeignClient framework reads the form at startup, builds the implementation, and handles every HTTP detail — connection management, serialization, retries, error decoding — entirely on its own. You never write a single line of HTTP plumbing code.


What Is FeignClient?

FeignClient is a declarative HTTP client originally developed by Netflix. In Spring Boot it is available through the spring-cloud-openfeign library.

  • Declarative — you define what to call, not how to call it.
  • interface driven — you write an interface with annotated methods; the framework generates the implementation at runtime.
  • Spring Cloud native — seamlessly integrates with Eureka (service discovery), Spring Cloud LoadBalancer, Spring Cloud Circuit Breaker, and API Gateway.

Why FeignClient Over RestTemplate / RestClient?

FeatureRestTemplateRestClientFeignClient
HTTP plumbingManualFluent, still manualZero
Service discovery integrationManual with DiscoveryClientManualAutomatic
Load balancingManual or @LoadBalancedManual or @LoadBalancedAutomatic
RetryManualManualbuilt in Retryer
Error handlingTry-catch or ResponseErrorHandleronStatus()ErrorDecoder
Code volumeHighMediumMinimal

The recommendation for Spring Cloud based microservices: use FeignClient as the primary synchronous communication mechanism.


Adding the Dependency

xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <!-- Version managed by Spring Cloud BOM — do not specify manually -->
</dependency>

Why Use Dependency Management (BOM)?

Spring Cloud consists of many libraries: OpenFeign, Eureka, LoadBalancer, Config Server, Gateway, etc. Each release of Spring Cloud publishes a BOM (Bill of Materials) that guarantees all the libraries within that release are mutually compatible. Without the BOM, you would need to manually verify that your chosen version of openfeign is compatible with your chosen version of eureka-client, which is error prone.

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

With this in place, all spring-cloud-* dependencies omit their <version> tags and Maven resolves compatible versions automatically.


Four Steps to Enable FeignClient

Step 1: Enable FeignClients on the Application Class

java
@SpringBootApplication
@EnableFeignClients  // tells Spring to scan for @FeignClient interfaces
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

Without @EnableFeignClients, Spring never scans for @FeignClient annotated interfaces and no proxies are created.

Step 2: Declare the FeignClient Interface

java
@FeignClient(
    name = "product-service",           // arbitrary name used in configuration lookups
    url = "${feign.client.product-service.url}"  // base URL from application.properties
)
public interface ProductClient {

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

    @PostMapping(value = "/product", consumes = MediaType.APPLICATION_JSON_VALUE)
    Product createProduct(@RequestBody Product product);

    @PutMapping(value = "/product/update/{id}", consumes = MediaType.APPLICATION_JSON_VALUE)
    Product updateProduct(
        @PathVariable("id") Long id,
        @RequestBody Product product,
        @RequestParam("sendMail") boolean sendMail,
        @RequestHeader("X-Custom-Header") String customHeader
    );

    @DeleteMapping("/product/{id}")
    void deleteProduct(@PathVariable("id") Long id);
}

Key observations:

  • Annotations (@GetMapping, @PostMapping, @PathVariable, @RequestBody) are identical to controller annotations. This makes the interface easy to read and aligns directly with the controller in the target service.
  • Parameter ordering does not need to match the controller. Feign maps parameters based on annotations, not position.
  • No implementation class is written anywhere.

Step 3: Add the Base URL to application.properties

properties
# order-service application.properties
server.port=8081
spring.application.name=order-service

feign.client.product-service.url=http://localhost:8082

Step 4: Autowire and Use

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

    private final ProductClient productClient;

    public OrderController(ProductClient productClient) {
        this.productClient = productClient;
    }

    @GetMapping("/{id}")
    public String placeOrder(@PathVariable Long id) {
        String product = productClient.getProductById(id);
        System.out.println("Product response: " + product);
        return "Order placed for: " + product;
    }
}

How It Works Internally: The Declarative Magic Explained

The question that naturally arises: we never wrote an implementation of ProductClient, yet Spring auto-wires it successfully. How?

Phase 1: Startup — Scanning and MethodHandler Creation

When the application starts and @EnableFeignClients is present, Feign scans all interfaces annotated with @FeignClient. For each interface, and for each method on that interface, it creates a MethodHandler object by parsing the method signature and annotations.

A MethodHandler holds:

targetUrl       = base URL + relative path (e.g., http://localhost:8082/product/{id})
httpMethod      = derived from @GetMapping / @PostMapping / etc.
headerInfo      = derived from @RequestHeader annotations
httpClient      = HttpUrlConnection by default (configurable)
encoder         = converts Java object → JSON (default: Jackson-based SpringEncoder)
decoder         = converts JSON → Java object (default: Jackson-based SpringDecoder)
errorDecoder    = handles non-2xx responses (default: FeignException wrapper)
logger          = logs request/response details
retryer         = controls retry behavior on connection failures

All MethodHandlers for a given interface are stored in a map: Map<Method, MethodHandler>.

Phase 2: InvocationHandler Creation

Feign creates an InvocationHandler object for each @FeignClient interface and populates it with the method-to-MethodHandler map. The InvocationHandler has one key method: invoke(proxy, method, args).

Phase 3: Dynamic Proxy Creation

Using Java's java.lang.reflect.Proxy, Feign generates a runtime implementation of the ProductClient interface. This generated class:

  • Implements ProductClient
  • Holds a reference to the InvocationHandler
  • For every method call, delegates to invocationHandler.invoke(this, method, args)

The InvocationHandler.invoke acts as a bridge: it looks up the correct MethodHandler from the map using the called method, then calls methodHandler.invoke(args), which constructs and executes the actual HTTP request.

This generated proxy object is registered as a Spring bean, which is why @Autowired works.


Custom Encoder and Decoder

By default, Feign uses Jackson for both encoding (Java → JSON) and decoding (JSON → Java). You can replace either with a custom implementation.

Custom Encoder

java
public class ProductEncoder implements Encoder {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
        try {
            // Convert Java object to JSON bytes and set as request body
            byte[] jsonBytes = objectMapper.writeValueAsBytes(object);
            template.body(Request.Body.encoded(jsonBytes, StandardCharsets.UTF_8));
        } catch (JsonProcessingException e) {
            throw new EncodeException("Failed to encode request body", e);
        }
    }
}

Custom Decoder

java
public class ProductDecoder implements Decoder {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public Object decode(Response response, Type type) throws IOException, DecodeException {
        if (response.body() == null) return null;
        try (InputStream is = response.body().asInputStream()) {
            return objectMapper.readValue(is, objectMapper.constructType(type));
        }
    }
}

Registering via Configuration

java
@Configuration
public class ProductClientConfig {

    @Bean
    public Encoder productEncoder() {
        return new ProductEncoder();
    }

    @Bean
    public Decoder productDecoder() {
        return new ProductDecoder();
    }
}
java
@FeignClient(
    name = "product-service",
    url = "${feign.client.product-service.url}",
    configuration = ProductClientConfig.class  // applies only to this FeignClient
)
public interface ProductClient { ... }

Per-client configuration means ProductClient uses ProductEncoder while other @FeignClient interfaces use the default Jackson encoder. Clean separation with no global impact.


Custom ErrorDecoder

The default ErrorDecoder wraps all non-2xx responses in a FeignException that includes status code, response body, and headers. This is often too generic for production services.

java
public class ProductErrorDecoder implements ErrorDecoder {

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

    @Override
    public Exception decode(String methodKey, Response response) {
        return switch (response.status()) {
            case 400 -> new BadRequestException(
                "Invalid request to product service: " + extractBody(response));
            case 404 -> new ProductNotFoundException(
                "Product not found. Method: " + methodKey);
            case 500, 503 -> new ProductServiceException(
                "Product service internal error: " + response.status());
            default -> defaultDecoder.decode(methodKey, response);
        };
    }

    private String extractBody(Response response) {
        try (InputStream is = response.body().asInputStream()) {
            return new String(is.readAllBytes(), StandardCharsets.UTF_8);
        } catch (IOException e) {
            return "<unreadable>";
        }
    }
}

Register in the client-specific configuration class and reference it from @FeignClient(configuration = ProductClientConfig.class).


Retryer Configuration

By default Feign retries up to 4 times (5 total attempts) on IOException and connection-timeout-related failures. Retries do NOT happen automatically for 4xx or 5xx HTTP status codes — those go directly to the ErrorDecoder.

Attempt 1 (immediate) → timeout
Attempt 2 (wait 100 ms) → timeout
Attempt 3 (wait 200 ms) → timeout
Attempt 4 (wait 400 ms) → timeout
Attempt 5 (wait 800 ms, capped at 1000 ms max) → failure → ErrorDecoder

Disable Retry Completely

java
@Bean
public Retryer retryer() {
    return Retryer.NEVER_RETRY;
}

Custom Retry Logic — Extend Default

java
public class ProductRetryer extends Retryer.Default {
    public ProductRetryer() {
        // maxAttempts=3, initialPeriod=200ms, maxPeriod=2000ms
        super(200, TimeUnit.SECONDS.toMillis(2), 3);
    }
}

Custom Retry Logic — Full Control

java
public class CustomRetryer implements Retryer {

    private int attempt = 0;
    private final int maxAttempts = 3;

    @Override
    public void continueOrPropagate(RetryableException e) {
        if (attempt++ >= maxAttempts) {
            throw e; // no more retries, propagate to ErrorDecoder
        }
        try {
            Thread.sleep(100L * attempt); // linear backoff
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            throw e;
        }
    }

    @Override
    public Retryer clone() {
        return new CustomRetryer(); // Feign requires a fresh instance per request
    }
}

Configuring Timeouts via application.properties

Timeouts can be set per named client or globally using default.

properties
# Applies only to the FeignClient named "product-service"
feign.client.config.product-service.connect-timeout=3000
feign.client.config.product-service.read-timeout=5000

# Applies to ALL FeignClient interfaces
feign.client.config.default.connect-timeout=5000
feign.client.config.default.read-timeout=10000

connect-timeout is the maximum time to establish the TCP connection. read-timeout is the maximum time to wait for a server response after the connection is open. Both are in milliseconds.


Interview Questions & Pitfalls

Q1. What does "declarative" mean in the context of FeignClient?

Declarative means you specify what you want (which endpoint to call, what parameters, what response type) without writing how to execute the HTTP call. The framework (Feign) provides the implementation based on your declarations (annotations). This is in contrast to RestTemplate or RestClient, where you explicitly write the HTTP call logic.

Q2. How does FeignClient autowire into a Spring bean if no implementation class exists?

At startup, @EnableFeignClients triggers FeignClient scanning. For each @FeignClient interface, the Feign framework uses Java's java.lang.reflect.Proxy to generate a runtime implementation. This implementation delegates method calls through an InvocationHandler to a MethodHandler, which builds and executes the HTTP request. The generated proxy object is registered as a Spring bean, making it available for @Autowired injection.

Q3. What happens when FeignClient receives a 4xx or 5xx response?

By default, a non-2xx response triggers the ErrorDecoder.decode() method. The default decoder wraps the response in a FeignException containing the status code, headers, and body. Custom ErrorDecoder implementations let you map specific HTTP status codes to domain-specific exceptions (e.g., 404 → ProductNotFoundException).

Q4. When does FeignClient retry a request?

Feign retries only on retriable exceptions — specifically IOException and connection timeout-related RetryableException. HTTP 4xx and 5xx responses do NOT trigger retries; they go directly to the ErrorDecoder. The default retryer makes up to 5 total attempts with exponential backoff starting at 100 ms, capped at 1 second.

Q5. What is the purpose of @FeignClient(configuration = ...) and how does it differ from global configuration?

The configuration attribute lets you apply custom beans (encoder, decoder, retryer, errorDecoder, logger) to a specific @FeignClient interface only. Beans defined in the configuration class are scoped to that one client. This is important because different downstream services may require different encoders, different timeout values, or different error handling strategies. Global defaults apply to all @FeignClient interfaces unless overridden by a client-specific configuration.

Q6. What is the MethodHandler and what information does it hold?

MethodHandler is an internal Feign class created for each method of a @FeignClient interface. It holds all the information needed to execute an HTTP request for that method: the target URL (base + path), the HTTP method, header mappings, path variable mappings, the encoder, the decoder, the errorDecoder, the retryer, and the underlying HTTP client. When a proxied method is invoked at runtime, the InvocationHandler looks up the corresponding MethodHandler in its map and delegates the actual HTTP execution to it.

Q7. Why is it important to name your @FeignClient correctly?

The name attribute serves as the service ID for load balancing and timeout configuration. When service discovery is in use (covered in the next chapter), Feign uses the name to discover instances from the registry. In application.properties, the name scopes timeout and configuration properties (e.g., feign.client.config.product-service.connect-timeout). Mismatching the name means load balancing resolves to the wrong service and configuration properties are ignored.