skip to content

How does Feign client configuration scoping work, and what pitfalls arise with default-config, contextId, and shared interfaces?

level: principalimportance: should knowfreq 40%

answer

  1. per-client child context (FeignClientFactory)
  2. precedence: defaults < global < per-client < properties
  3. default-to-properties=true → yaml wins
  4. same name needs distinct contextId
  5. shared @RestController/@FeignClient interface = discouraged

basics

~20 s

Each Feign client gets its own child application context holding its Encoder, Decoder, Contract, interceptors, etc. Beans in @FeignClients(defaultConfiguration=...) or the client's own configuration apply per-client; a @Configuration under component scan applies globally. Properties in application.yml can override code config.

solid answer

~40 s

Spring Cloud OpenFeign creates a **per-client child context** (a `FeignClientFactory` / named context) so each `@FeignClient` can have its own `Encoder`, `Decoder`, `Contract`, `ErrorDecoder`, `RequestInterceptor`s, `Retryer`, `Logger.Level`, options, etc. Precedence, low to high: framework defaults → global `@Configuration` beans in the component-scan path (or `@EnableFeignClients(defaultConfiguration=...)`) → a client's `@FeignClient(configuration=...)` beans → `application.yml` `feign.client.config.<name>` (and `default`) properties, which by default **win over** Java config unless you set `feign.client.default-to-properties=false`. A classic pitfall: annotating a per-client `configuration` class with `@Configuration` while it sits under the scanned packages — it then leaks globally to every client. Another: two `@FeignClient`s sharing the same `name` need distinct `contextId`s or bean-name clashes occur. Sharing an interface between server (`@RestController`) and client is tempting but couples deployments and is generally discouraged.

code

java · 21 lines
java
// Global defaults for every client
@EnableFeignClients(defaultConfiguration = GlobalFeignConfig.class)
@SpringBootApplication
public class App { }

public class GlobalFeignConfig {
    @Bean
    public RequestInterceptor tracing(Tracer tracer) {
        return t -> t.header("X-Request-Id", tracer.currentTraceId());
    }
    @Bean
    public feign.Logger.Level level() { return feign.Logger.Level.BASIC; }
}

// Two interfaces, same service, distinct contextId to avoid clashes
@FeignClient(name = "catalog", contextId = "catalogProducts")
interface ProductClient { /* ... */ }

@FeignClient(name = "catalog", contextId = "catalogInventory",
             configuration = InventoryFeignConfig.class) // per-client override
interface InventoryClient { /* ... */ }

go deeper

for a junior

Know that config can be global or per-client and that application.yml can set timeouts/log level.

for a middle

Explain @FeignClient(configuration=...) vs global @Configuration and the accidental global-leak pitfall.

for a senior

Detail the precedence order including default-to-properties, contextId for duplicate names, and Logger.Level+DEBUG.

for a principal

Architect a default-config + targeted-override strategy, avoid shared annotated interfaces, and design consistent cross-cutting concerns at platform scale.

## Per-client child contexts Spring Cloud OpenFeign builds a **named child `ApplicationContext` per client** via the `FeignClientFactory` (historically `FeignContext`). Each context can hold its own copy of the Feign building blocks: `Encoder`, `Decoder`, `Contract`, `ErrorDecoder`, `RequestInterceptor`(s), `Retryer`, `Client`, `Request.Options`, `Logger.Level`, `QueryMapEncoder`, `ExceptionPropagationPolicy`. This isolation is *why* one client can speak multipart while another speaks JSON, or one can carry an auth interceptor others don't. ## Configuration precedence (lowest → highest) 1. **Framework auto-config** — `FeignClientsConfiguration` supplies sensible defaults (SpringEncoder/Decoder, SpringMvcContract, `Retryer.NEVER_RETRY`, `Logger.Level.NONE`). 2. **Global Java config** — beans in a `@Configuration` on the component-scan path, or set via `@EnableFeignClients(defaultConfiguration = GlobalFeignConfig.class)`. These apply to *all* clients. 3. **Per-client Java config** — `@FeignClient(configuration = MyClientConfig.class)`. Beans here override globals **for that client only**. Crucially, this class should **not** be `@Configuration`-annotated *and* component-scanned, or it leaks globally. 4. **Properties** — `feign.client.config.<clientName>.*` (or `feign.client.config.default.*`) in `application.yml`. By default `feign.client.default-to-properties=true`, so **properties override Java config**. Flip it to `false` to let Java config win. ### Property example ``` feign: client: config: default: loggerLevel: basic connectTimeout: 2000 readTimeout: 5000 catalog: loggerLevel: full errorDecoder: com.acme.CatalogErrorDecoder ``` ## contextId and duplicate names Two `@FeignClient` interfaces with the **same `name`** (e.g. splitting a big API across interfaces pointing at one service) will collide on bean/context naming. Set a distinct **`contextId`** on each: ``` @FeignClient(name = "catalog", contextId = "catalogProducts") @FeignClient(name = "catalog", contextId = "catalogInventory") ``` The `name` still drives URL/discovery; `contextId` disambiguates the config context and bean name. ## Eager vs lazy context creation Client contexts are created lazily on first use by default, which can hide misconfiguration until runtime. You can force eager init (`spring.cloud.openfeign.lazy-attributes-resolution` / client-context eager settings across versions) to fail fast at startup. ## Logging Feign's `Logger.Level` (NONE/BASIC/HEADERS/FULL) is **not** a logging framework level — it controls how much request/response detail Feign emits, and it only emits at DEBUG for the client interface's logger. You must both set the level (bean or property) **and** enable DEBUG logging for the interface's package. ## Shared-interface anti-pattern A tempting DRY move is to define one interface, implement it in the provider as a `@RestController` and reuse it as the `@FeignClient` in consumers. Spring's docs explicitly caution against this: it tightly couples producer and consumer deployment/versioning, breaks independent evolution, and mixes server and client annotation semantics. Prefer sharing a thin DTO/API module (or a published OpenAPI contract) over sharing the annotated interface. ## Pitfall checklist - Per-client config class wrongly `@Configuration` + scanned → global leak. - Expecting Java `@Bean` to win when a `feign.client.config` property silently overrides it. - Same `name` without distinct `contextId` → context/bean clash. - Setting `Logger.Level.FULL` but seeing nothing → interface logger not at DEBUG. - Assuming a `Retryer` exists — default is NEVER_RETRY. - Timeouts: `connectTimeout`/`readTimeout` live in `Request.Options`; per-client property overrides global. ## When it matters At platform scale you standardize cross-cutting concerns (auth, tracing, timeouts, error mapping) via a shared **default configuration** while allowing targeted per-client overrides — getting the scoping and precedence right is what keeps that maintainable.

  • You set an ErrorDecoder as a @Bean in the client's configuration class, but a feign.client.config property picks a different one. Which wins and why?
    By default the property wins, because `feign.client.default-to-properties=true` makes application.yml Feign config take precedence over Java @Bean config. To make Java config authoritative, set `feign.client.default-to-properties=false` (then properties only fill gaps).
  • Why does Spring caution against reusing one annotated interface as both the server @RestController and the client @FeignClient?
    It tightly couples producer and consumer: they must share and co-version the interface, preventing independent evolution and deployment, and it conflates server- and client-side annotation semantics. Sharing a DTO module or a versioned OpenAPI contract is safer than sharing the annotated interface.
  • You set Logger.Level.FULL but see no request logs. What's missing?
    Feign only emits its request/response logs at DEBUG level for the client *interface's* own logger. You must enable DEBUG logging for that interface's package/class in addition to setting the Feign Logger.Level; the level alone does nothing without DEBUG logging enabled.

saying these in an interview costs you the question

  • Believing all clients share one global Feign context/config
  • Assuming Java @Bean config always overrides application.yml (it's the opposite by default)
  • Thinking two @FeignClients can share a name with no contextId
  • Treating Feign Logger.Level as a logging framework level rather than a detail selector needing DEBUG
  • Recommending shared server/client annotated interfaces as best practice

context