Spring Cloud OpenFeign - Quick Reference

SkillAI & models

Declarative HTTP client for microservices communication with Spring Cloud OpenFeign. Covers @FeignClient, error handling, interceptors, and circuit breaker integration.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Spring Cloud OpenFeign - Quick Reference skill

What this skill tells your AI

The instructions your AI receives, as published by claude-dev-suite/claude-dev-suite in skills/backend-frameworks/spring-cloud-openfeign/SKILL.md and read by ahel’s review.

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: spring-cloud-openfeign for comprehensive documentation.

Dependencies

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- For load balancing -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

Enable Feign Clients

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

// Or scan specific packages
@EnableFeignClients(basePackages = "com.example.clients")

Basic Feign Client

@FeignClient(name = "user-service")
public interface UserClient {

    @GetMapping("/api/users/{id}")
    UserResponse getUserById(@PathVariable("id") Long id);

    @GetMapping("/api/users")
    List<UserResponse> getAllUsers();

    @GetMapping("/api/users")
    List<UserResponse> getUsersByStatus(@RequestParam("status") String status);

    @PostMapping("/api/users")
    UserResponse createUser(@RequestBody CreateUserRequest request);

    @PutMapping("/api/users/{id}")
    UserResponse updateUser(@PathVariable("id") Long id, @RequestBody UpdateUserRequest request);

    @DeleteMapping("/api/users/{id}")
    void deleteUser(@PathVariable("id") Long id);
}

Feign with URL (No Service Discovery)

@FeignClient(name = "external-api", url = "${external.api.url}")
public interface ExternalApiClient {

    @GetMapping("/data")
    DataResponse getData(@RequestHeader("Authorization") String token);
}

Configuration

application.yml

spring:
  cloud:
    openfeign:
      client:
        config:
          default:  # Apply to all clients
            connect-timeout: 5000
            read-timeout: 10000
            logger-level: BASIC

          user-service:  # Specific client
            connect-timeout: 3000
            read-timeout: 5000
            logger-level: FULL

      circuitbreaker:
        enabled: true

      micrometer:
        enabled: true

# Logging
logging:
  level:
    com.example.clients: DEBUG

Java Configuration

@Configuration
public class FeignConfig {

    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;  // NONE, BASIC, HEADERS, FULL
    }

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

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

    @Bean
    public Request.Options options() {
        return new Request.Options(5, TimeUnit.SECONDS, 10, TimeUnit.SECONDS, true);
    }
}

// Apply to specific client
@FeignClient(name = "user-service", configuration = FeignConfig.class)
public interface UserClient { }

Request/Response Interceptors

Request Interceptor

@Component
public class AuthRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // Add auth header to all requests
        String token = SecurityContextHolder.getContext()
            .getAuthentication().getCredentials().toString();
        template.header("Authorization", "Bearer " + token);

        // Add correlation ID
        template.header("X-Correlation-Id", MDC.get("correlationId"));
    }
}

Client-Specific Interceptor

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

@Configuration
public class PaymentClientConfig {

    @Bean
    public RequestInterceptor paymentAuthInterceptor() {
        return template -> {
            template.header("X-Api-Key", apiKey);
        };
    }
}

Error Handling

Custom Error Decoder

public class CustomErrorDecoder implements ErrorDecoder {

    private final ErrorDecoder defaultDecoder = new Default();

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

        switch (status) {
            case NOT_FOUND:
                return new ResourceNotFoundException(
                    "Resource not found: " + methodKey);
            case BAD_REQUEST:
                return new BadRequestException(
                    "Bad request: " + getBody(response));
            case UNAUTHORIZED:
                return new UnauthorizedException("Unauthorized");
            case SERVICE_UNAVAILABLE:
                return new ServiceUnavailableException(
                    "Service unavailable");
            default:
                return defaultDecoder.decode(methodKey, response);
        }
    }

    private String getBody(Response response) {
        try {
            return Util.toString(response.body().asReader(StandardCharsets.UTF_8));
        } catch (Exception e) {
            return "";
        }
    }
}

Global Exception Handler

@ControllerAdvice
public class FeignExceptionHandler {

    @ExceptionHandler(FeignException.class)
    public ResponseEntity<ErrorResponse> handleFeignException(FeignException e) {
        HttpStatus status = HttpStatus.valueOf(e.status());

        return ResponseEntity
            .status(status)
            .body(new ErrorResponse(
                status.value(),
                "Downstream service error",
                e.getMessage()
            ));
    }
}

Fallback

With Fallback Class

@FeignClient(
    name = "user-service",
    fallback = UserClientFallback.class
)
public interface UserClient {
    @GetMapping("/api/users/{id}")
    UserResponse getUserById(@PathVariable Long id);
}

@Component
public class UserClientFallback implements UserClient {

    @Override
    public UserResponse getUserById(Long id) {
        return UserResponse.builder()
            .id(id)
            .name("Unknown User")
            .status("FALLBACK")
            .build();
    }
}

With FallbackFactory (Access to Exception)

@FeignClient(
    name = "user-service",
    fallbackFactory = UserClientFallbackFactory.class
)
public interface UserClient {
    @GetMapping("/api/users/{id}")
    UserResponse getUserById(@PathVariable Long id);
}

@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {

    @Override
    public UserClient create(Throwable cause) {
        return new UserClient() {
            @Override
            public UserResponse getUserById(Long id) {
                log.error("Fallback triggered for user {}: {}", id, cause.getMessage());

                if (cause instanceof FeignException.ServiceUnavailable) {
                    return UserResponse.cached(id);  // Return cached version
                }

                return UserResponse.unknown(id);
            }
        };
    }
}

Circuit Breaker Integration

With Resilience4j

spring:
  cloud:
    openfeign:
      circuitbreaker:
        enabled: true

resilience4j:
  circuitbreaker:
    configs:
      default:
        slidingWindowSize: 10
        failureRateThreshold: 50
        waitDurationInOpenState: 10000
        permittedNumberOfCallsInHalfOpenState: 3
    instances:
      user-service:
        baseConfig: default
        failureRateThreshold: 30

  timelimiter:
    configs:
      default:
        timeoutDuration: 5s

Headers and Parameters

@FeignClient(name = "api-service")
public interface ApiClient {

    // Path variable
    @GetMapping("/items/{id}")
    Item getItem(@PathVariable("id") String id);

    // Query parameters
    @GetMapping("/items")
    List<Item> searchItems(
        @RequestParam("q") String query,
        @RequestParam(value = "page", defaultValue = "0") int page,
        @RequestParam(value = "size", defaultValue = "20") int size);

    // Headers
    @GetMapping("/secure/data")
    Data getData(
        @RequestHeader("Authorization") String auth,
        @RequestHeader("X-Request-Id") String requestId);

    // Request body
    @PostMapping("/items")
    Item createItem(@RequestBody CreateItemRequest request);

    // Form data
    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    void uploadFile(@RequestPart("file") MultipartFile file);

    // Multiple parts
    @PostMapping(value = "/submit", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    void submitForm(@RequestBody Map<String, ?> formData);
}

Query Map

@FeignClient(name = "search-service")
public interface SearchClient {

    @GetMapping("/search")
    SearchResult search(@SpringQueryMap SearchCriteria criteria);
}

@Data
public class SearchCriteria {
    private String query;
    private Integer page;
    private Integer size;
    private String sortBy;
    private String sortOrder;
}

// Usage
SearchCriteria criteria = new SearchCriteria();
criteria.setQuery("test");
criteria.setPage(0);
criteria.setSize(20);
searchClient.search(criteria);
// Generates: /search?query=test&page=0&size=20

Testing

Mock with WireMock

@SpringBootTest
@AutoConfigureWireMock(port = 0)
class UserClientTest {

    @Autowired
    private UserClient userClient;

    @Test
    void shouldGetUser() {
        stubFor(get(urlPathEqualTo("/api/users/1"))
            .willReturn(aResponse()
                .withStatus(200)
                .withHeader("Content-Type", "application/json")
                .withBody("""
                    {"id": 1, "name": "John", "email": "john@example.com"}
                """)));

        UserResponse user = userClient.getUserById(1L);

        assertThat(user.getName()).isEqualTo("John");
    }

    @Test
    void shouldHandleError() {
        stubFor(get(urlPathEqualTo("/api/users/999"))
            .willReturn(aResponse().withStatus(404)));

        assertThatThrownBy(() -> userClient.getUserById(999L))
            .isInstanceOf(ResourceNotFoundException.class);
    }
}

Best Practices

DoDon't
Use service discovery namesHardcode URLs
Configure timeoutsUse infinite timeouts
Implement fallbacksLet failures cascade
Use error decodersIgnore error responses
Add request interceptorsDuplicate auth logic

Production Checklist

  • Timeouts configured
  • Circuit breaker enabled
  • Fallbacks implemented
  • Error decoder configured
  • Logging level appropriate
  • Auth interceptor added
  • Retry policy set
  • Metrics enabled
  • Load balancer configured
  • Connection pool tuned

When NOT to Use This Skill

  • External APIs - Consider WebClient, RestClient
  • Reactive - Use WebClient instead
  • Simple calls - RestClient may be simpler
  • File uploads - May need custom config

Anti-Patterns

Anti-PatternProblemSolution
No timeout configuredHanging requestsSet connectTimeout, readTimeout
Missing error decoderSwallowed errorsImplement ErrorDecoder
No circuit breakerCascading failuresIntegrate Resilience4j
Blocking threadThread exhaustionUse async or circuit breaker
Hardcoded URLsNot using discoveryUse service name

Quick Troubleshooting

ProblemDiagnosticFix
Service not foundCheck discoveryVerify Eureka registration
TimeoutCheck timeout configIncrease or fix service
404 on callCheck pathVerify @RequestMapping path
Serialization errorCheck content typeConfigure Jackson converter
Auth failingCheck interceptorAdd RequestInterceptor

Reference Documentation

Signals

GitHub stars
33
Forks
6
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
spring-cloud-openfeign
Source
github.com/claude-dev-suite/claude-dev-suite