Skip to content
Spring Boot sb web 3 min read

OpenFeign Client

Spring Cloud OpenFeign turns an HTTP API into a Java interface: you declare methods with Spring MVC annotations and Feign generates the calling code at runtime. It removes boilerplate compared to hand-written clients and is popular in microservices. This page covers setup, @FeignClient, configuration, and error decoding. For the general HTTP-client landscape see WebClient & RestClient.

Dependencies and enabling

Add the Spring Cloud OpenFeign starter and import the Spring Cloud BOM so versions align with your Spring Boot version.

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

Enable client scanning on a configuration class:

import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableFeignClients
public class FeignConfig {
}

Note: OpenFeign is part of Spring Cloud, so its release train (e.g. 2025.0.x) must match your Spring Boot 3.5 line. Always use the BOM rather than pinning a raw version.

Declaring a @FeignClient

Define an interface, annotate it, and use the same MVC annotations you would on a controller. Feign implements it as a bean you can inject.

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;

public record User(Long id, String name, String email) {}
public record CreateUser(String name, String email) {}

@FeignClient(name = "user-service", url = "${clients.user-service.url}")
public interface UserClient {

    @GetMapping("/users/{id}")
    User getUser(@PathVariable Long id);

    @GetMapping("/users")
    List<User> list(@RequestParam(defaultValue = "0") int page);

    @PostMapping("/users")
    User create(@RequestBody CreateUser body);
}
clients.user-service.url=https://api.example.com

Inject and call it like any bean — no HTTP plumbing in your service:

@Service
public class ProfileService {

    private final UserClient userClient;

    public ProfileService(UserClient userClient) {
        this.userClient = userClient;
    }

    public User profile(Long id) {
        return userClient.getUser(id);
    }
}

Request issued by Feign:

GET https://api.example.com/users/42
Accept: application/json

Output (deserialized):

{ "id": 42, "name": "Ada Lovelace", "email": "[email protected]" }

Tip: Omit url and use only name when combined with service discovery (e.g. Eureka) — Feign then resolves the host through the load balancer. See Service Discovery.

Configuration

Tune timeouts and logging globally in application.yml:

spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            connectTimeout: 2000
            readTimeout: 5000
            loggerLevel: basic
          user-service:
            readTimeout: 8000

Feign’s logger levels are none, basic, headers, and full. Logging only emits when the surrounding logger is at DEBUG:

logging.level.com.example.client.UserClient=DEBUG

You can also supply a per-client Java configuration class for custom beans (interceptors, decoders, contracts):

@FeignClient(name = "user-service", url = "${clients.user-service.url}",
        configuration = UserClientConfig.class)
public interface UserClient { ... }
public class UserClientConfig {

    @Bean
    public RequestInterceptor authInterceptor() {
        return template -> template.header("Authorization", "Bearer " + TokenHolder.get());
    }
}

Warning: A configuration class referenced from @FeignClient should not be @Configuration annotated, or its beans leak into the global context and apply to every client.

Error decoding

By default Feign throws FeignException on non-2xx responses. Supply an ErrorDecoder to map upstream statuses to your own exceptions.

import feign.Response;
import feign.codec.ErrorDecoder;

public class UserErrorDecoder implements ErrorDecoder {

    private final ErrorDecoder defaultDecoder = new Default();

    @Override
    public Exception decode(String methodKey, Response response) {
        return switch (response.status()) {
            case 404 -> new NotFoundException("User not found");
            case 400 -> new BadRequestException("Invalid request to user-service");
            default  -> defaultDecoder.decode(methodKey, response);
        };
    }
}

Register it in the client’s configuration:

public class UserClientConfig {
    @Bean
    public ErrorDecoder errorDecoder() {
        return new UserErrorDecoder();
    }
}

OpenFeign vs RestClient/WebClient

AspectOpenFeignRestClient / WebClient
StyleDeclarative interfaceImperative fluent calls
BoilerplateMinimalMore per call
DependencySpring CloudCore Spring
Discovery / LB integrationBuilt inManual / via load balancer
Best forMany service-to-service callsOne-off or fine-grained control

Pitfalls

  • Forgetting @EnableFeignClients means the interfaces are never proxied — injection fails at startup.
  • Mismatched Spring Cloud and Spring Boot versions cause obscure runtime errors; rely on the BOM.
  • Feign uses Spring MVC annotations but it is a client, so @PathVariable/@RequestParam describe the outgoing request, not an incoming one.
Last updated June 13, 2026
Was this helpful?