Ch. 8 · Spring Boot

Spring Declarative HTTP Clients

Define outbound HTTP as an annotated interface with @HttpExchange, create the proxy, and configure timeouts and errors.

~2 min readadvancedupdated Oct 5, 2026

Spring’s declarative HTTP client lets you describe a remote API as a Java interface annotated with @HttpExchange. Spring generates the proxy, so callers use typed methods instead of hand-written request code, and the contract lives in one place.

Before you start

You should be comfortable with Spring beans and dependency injection, and know basic HTTP semantics. This article assumes Spring Framework 6 or later for @HttpExchange.

Step-by-step walkthrough

Step 1: Describe the API as an interface

Annotate the interface with @HttpExchange("/users") and each method with @GetExchange, @PostExchange and so on. Parameters map to path variables, query parameters or the request body, so the method signature documents the call.

Step 2: Create the proxy bean

Register a bean using HttpServiceProxyFactory with an underlying RestClient or WebClient, and set the base URL. Inject the interface wherever it is needed; callers depend on the interface, not on the HTTP library, which keeps them testable.

Step 3: Configure timeouts and error mapping

Set connect and read timeouts on the underlying client, and define how non-2xx responses map to exceptions. A default of no timeout can hang a request thread indefinitely, so timeouts are part of the client contract rather than an afterthought.

Worked scenario

The interface declares the remote call; Spring supplies the implementation.

@HttpExchange("/users")
public interface UserClient {

  @GetExchange("/{id}")
  User getById(@PathVariable String id);
}
java

Walk through the example

@HttpExchange sets the base path, and @GetExchange("/{id}") maps the method to GET /users/{id}. Spring resolves id into the path and deserializes the response into User. A caller injects UserClient and calls getById as a normal method, so the HTTP details stay in the interface.

Common mistake

Leaving the underlying client without timeouts, which lets one slow dependency exhaust the request thread pool. Another is forgetting that this is a blocking client and calling it from a reactive context, where a non-blocking client is required.

Verify the behavior

Point the base URL at a mock server and assert the request path, method and body match the interface. Return a 500 and confirm it maps to the expected exception. Delay the mock response beyond the configured timeout and confirm the call fails with a timeout instead of hanging.

Interview exercise

Why put the timeout on the client bean rather than in each call site?

Answer and reasoning

A timeout is part of the client’s contract: every call to that dependency should stop waiting at the same point, and a default in one place avoids the call sites that forget it. Per-call overrides remain possible for known-long operations, but the safe default belongs on the shared bean so a hang cannot exhaust the thread pool because one developer omitted a value.

Continue learning

Compare outbound call resilience in HTTP client timeouts and Exception advice. Read the Spring declarative HTTP interfaces documentation and try the Spring Boot interview questions.

More in Spring Boot

read ✓Spring Boot · hard

Spring @Async and Executor Configuration

Run methods asynchronously with @Async, configure a bounded executor, and handle exceptions and the proxy boundary.

~2 min readread →
read ✓Spring Boot · hard

Spring Boot Caching Abstraction

Cache method results with @Cacheable, choose keys and TTLs, and evict on writes without the self-invocation trap.

~2 min readread →
read ✓Spring Boot · mid

Spring Boot Problem Details for APIs

Return consistent RFC 7807 error responses, map exceptions centrally, and avoid leaking internals to clients.

~2 min readread →
esc