skip to content

At scale, how do you register many HTTP Interface clients cleanly, share configuration, and when would you NOT use declarative HTTP interfaces?

level: principalimportance: nice to knowfreq 22%

answer

  1. centralize RestClient.Builder, clone per base URL
  2. group registration (6.2) for many clients
  3. resilience via interceptors/Resilience4j, not built-in
  4. skip for dynamic/streaming/one-off calls
  5. native alternative to OpenFeign

basics

~20 s

Share 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 s

For 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
java
@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

for a junior

Aware that many clients each become a bean.

for a middle

Share a builder and know resilience isn't built in.

for a senior

Centralize cross-cutting config via interceptors and reason about testing/consistency trade-offs.

for a principal

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.

context