At scale, how do you register many HTTP Interface clients cleanly, share configuration, and when would you NOT use declarative HTTP interfaces?
answer
- centralize RestClient.Builder, clone per base URL
- group registration (6.2) for many clients
- resilience via interceptors/Resilience4j, not built-in
- skip for dynamic/streaming/one-off calls
- native alternative to OpenFeign
basics
~20 sShare a pre-configured RestClient.Builder and register each interface as an @Bean via one factory (or use Spring's group registration). Skip declarative interfaces when calls are highly dynamic, need per-call streaming/low-level control, or when one-off imperative RestClient calls are simpler.
solid answer
~40 sFor a few clients, expose each proxy as an `@Bean` built from a shared, centrally-configured `RestClient.Builder` (base config, interceptors for auth/tracing/metrics, timeouts) so all clients inherit cross-cutting behavior. Reuse one `HttpServiceProxyFactory`. For many clients, Spring Framework 6.2's group-based registration (`HttpServiceProxyRegistry` and, in Boot, declarative import of client groups) lets you register interface sets against named client configurations and auto-create proxies, keeping wiring DRY. Resilience (retries, circuit breakers, rate limiting) isn't built in — layer it via client interceptors/filters or Resilience4j around the proxy. When NOT to use it: highly dynamic requests (URL/params computed per call), streaming/low-level response control, when you need per-request customization the annotation model can't express, or trivial one-off calls where an inline `RestClient` is clearer than an interface.
code
java · 18 lines@Configuration
class HttpClients {
@Bean
RestClient.Builder appBuilder() {
return RestClient.builder()
.requestInterceptor(new BearerTokenInterceptor());
}
private <T> T create(RestClient.Builder b, String base, Class<T> type) {
RestClient rc = b.clone().baseUrl(base).build();
return HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(rc)).build()
.createClient(type);
}
@Bean UserClient userClient(RestClient.Builder b) { return create(b, "https://users.svc", UserClient.class); }
@Bean OrderClient orderClient(RestClient.Builder b) { return create(b, "https://orders.svc", OrderClient.class); }
}go deeper
Aware that many clients each become a bean.
Share a builder and know resilience isn't built in.
Centralize cross-cutting config via interceptors and reason about testing/consistency trade-offs.
Design registration at scale (group config), a resilience/observability layer, and articulate when declarative clients are the wrong tool vs raw RestClient or Feign.
## Sharing configuration across clients The key principle: **cross-cutting concerns live on the underlying client**, so centralize the client builder. ```java @Bean RestClient.Builder appRestClientBuilder(ObservationRegistry registry) { return RestClient.builder() .requestInterceptor(new BearerTokenInterceptor()) .requestInterceptor(new TracingInterceptor(registry)) .requestFactory(clientHttpRequestFactoryWithTimeouts()); } @Bean UserClient userClient(RestClient.Builder b) { return proxy(b, "https://users", UserClient.class); } @Bean OrderClient orderClient(RestClient.Builder b) { return proxy(b, "https://orders", OrderClient.class); } private <T> T proxy(RestClient.Builder b, String base, Class<T> type) { RestClient rc = b.clone().baseUrl(base).build(); return HttpServiceProxyFactory .builderFor(RestClientAdapter.create(rc)).build() .createClient(type); } ``` Every client inherits auth, tracing, and timeouts; only the base URL differs. ## Group registration (scale) Spring Framework 6.2 introduced a **registry/group** model: you declare groups of HTTP-service interfaces, each associated with a client configuration, and Spring builds the proxies and exposes a `HttpServiceProxyRegistry` to look them up. In Spring Boot this appears as declarative import of client groups (e.g. importing a set of interfaces bound to a named client), reducing per-interface `@Bean` boilerplate. Use it when you have many clients or want per-group base URLs/config; the manual `@Bean` approach is fine for a handful. ## Resilience & observability None of retries, circuit breaking, bulkheads, or rate limiting are part of HTTP Interfaces. Add them: - **Interceptors/filters** on the RestClient/WebClient (auth, correlation IDs, metrics via Micrometer `Observation`). - **Resilience4j** wrapping the client or the calling method (`@Retry`, `@CircuitBreaker`). - **Load balancing** (Spring Cloud LoadBalancer) by using a load-balanced `RestClient.Builder`. ## When NOT to use declarative interfaces - **Highly dynamic requests**: URL, method, or header set computed at runtime in ways the static annotation model can't express cleanly (though `URI`/`HttpMethod` parameters help a bit). - **Streaming / low-level control**: need the raw response stream, custom exchange logic, or per-call codec tweaks — use `RestClient`/`WebClient` imperatively. - **One-off calls**: a single call in one place is often clearer inline than a dedicated interface + bean. - **Non-REST protocols** or heavy content negotiation edge cases. - When the team already standardizes on **Spring Cloud OpenFeign** and doesn't want two client styles. ## Trade-offs vs alternatives - **vs raw RestClient**: declarative wins on readability, testability (mockable interface), and consistency; loses some per-call flexibility. - **vs OpenFeign**: HTTP Interfaces are native (no Spring Cloud dependency), share Spring's converters and RestClient infra, and support reactive; Feign has a larger ecosystem of built-in resilience/decoders. For new Spring 6+ apps, HTTP Interfaces are usually preferred. ## Gotchas - `clone()` the shared builder before setting a per-client base URL, or clients clobber each other. - Interceptor ordering matters (auth before tracing, etc.). - Don't rebuild proxies per request — they're singletons; wire once. - Group config is version-sensitive (6.2+/recent Boot) — confirm your Spring version before relying on it.
- How do you add retries and a circuit breaker to a declarative client?They aren't built in. Layer Resilience4j (e.g. @Retry/@CircuitBreaker on the calling service method) or wrap the client, and/or add request interceptors/filters on the underlying RestClient/WebClient. Load balancing comes from a load-balanced RestClient.Builder.
- Why clone() the shared RestClient.Builder per client?A builder is mutable; setting baseUrl on the shared instance would affect every client. clone() gives each proxy its own base URL while inheriting the shared interceptors/config.
- Give a case where you'd skip HTTP Interfaces entirely.Highly dynamic requests or streaming with low-level response control, or a single one-off call where an inline RestClient invocation is simpler than defining an interface plus a bean.
saying these in an interview costs you the question
- Claiming retries/circuit breaking are built into HTTP Interfaces.
- Setting baseUrl on a shared builder without clone(), causing clients to interfere.
- Insisting declarative interfaces should be used for every call regardless of dynamism.
- Believing you must use Spring Cloud OpenFeign to get declarative clients in Spring.