Spring Boot
WebClient
HTTP Client
retrieve vs exchange
Java Development

Spring boot Webclient's retrieve vs exchange

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

With Spring WebClient, the current practical choice is usually retrieve() for body-centric calls and exchangeToMono or exchangeToFlux when status codes or headers drive control flow. Older discussions often compare retrieve() with exchange(), but modern code should treat exchange() as legacy and prefer the newer exchange-to-body variants.

Use retrieve() for the Common Case

If your code wants “send request, decode body, raise an error for unsuccessful status,” retrieve() is the cleanest API.

java
1import org.springframework.web.reactive.function.client.WebClient;
2import reactor.core.publisher.Mono;
3
4public class UserClient {
5    private final WebClient client;
6
7    public UserClient(WebClient client) {
8        this.client = client;
9    }
10
11    public Mono<UserDto> getUser(long id) {
12        return client.get()
13            .uri("/users/{id}", id)
14            .retrieve()
15            .bodyToMono(UserDto.class);
16    }
17}

This is concise and readable. For many service-to-service calls, it is the right default.

Customize Errors With onStatus

retrieve() is not “all or nothing.” You can still map particular statuses into domain-specific exceptions.

java
1return client.get()
2    .uri("/users/{id}", id)
3    .retrieve()
4    .onStatus(status -> status.value() == 404,
5        response -> Mono.error(new UserNotFoundException(id)))
6    .onStatus(status -> status.value() >= 500,
7        response -> Mono.error(new UpstreamServerException("user-service")))
8    .bodyToMono(UserDto.class);

This keeps the happy path short while still handling expected failure modes explicitly.

Use exchangeToMono When Metadata Matters

If your behavior depends on the full response, not just the decoded body, use exchangeToMono.

java
1return client.get()
2    .uri("/users/{id}", id)
3    .exchangeToMono(response -> {
4        int code = response.statusCode().value();
5
6        if (code == 200) {
7            return response.bodyToMono(UserDto.class);
8        }
9        if (code == 404) {
10            return Mono.empty();
11        }
12        if (code == 429) {
13            String retryAfter = response.headers()
14                .asHttpHeaders()
15                .getFirst("Retry-After");
16            return Mono.error(new RateLimitedException(retryAfter));
17        }
18
19        return response.createException().flatMap(Mono::error);
20    });

This is the right tool when status codes, headers, or response shape determine what the client should do next.

Use exchangeToFlux for Streaming Sequences

If the response is a stream or sequence, the same principle applies with exchangeToFlux.

java
1return client.get()
2    .uri("/events")
3    .exchangeToFlux(response -> {
4        if (response.statusCode().is2xxSuccessful()) {
5            return response.bodyToFlux(EventDto.class);
6        }
7        return response.createException().flatMapMany(reactor.core.publisher.Flux::error);
8    });

This makes the response-handling contract explicit for streamed payloads.

Why Not Use Old exchange()

Legacy code may still show exchange(), but modern Spring guidance moved toward exchangeToMono and exchangeToFlux because they encourage clearer handling of the response lifecycle. In current codebases, the useful decision is not retrieve() versus exchange(). It is “simple body mapping” versus “full response-driven logic.”

A Good Team Default

Most teams benefit from a default rule:

  • use retrieve() for standard JSON body flows
  • use exchangeToMono or exchangeToFlux when response metadata affects behavior
  • document unusual error handling close to the client method

That keeps client code consistent and reduces stylistic churn during reviews.

Timeouts, Retries, and Responsibility Boundaries

The choice between retrieve() and exchangeToMono does not replace normal client resilience concerns. Timeouts, retries, and fallback rules still need to be handled explicitly.

java
1import reactor.util.retry.Retry;
2import java.time.Duration;
3
4return getUser(id)
5    .timeout(Duration.ofSeconds(2))
6    .retryWhen(
7        Retry.backoff(2, Duration.ofMillis(200))
8            .filter(ex -> ex instanceof UpstreamServerException)
9    );

Do not retry blindly. A 404 or validation failure is not the same kind of problem as a transient upstream outage.

Common Pitfalls

A common mistake is using exchangeToMono for every endpoint and turning simple client methods into verbose status-switching code.

Another mistake is using retrieve() when logic depends on headers or multiple status-specific branches. That usually leads to awkward code later.

It is also easy to treat all non-2xx results as one generic failure. Good client code makes the important status distinctions explicit.

Summary

  • Use retrieve() by default when you just want to decode a successful body.
  • Add onStatus(...) when you need a few explicit error mappings.
  • Use exchangeToMono or exchangeToFlux when status codes or headers drive behavior.
  • Treat old exchange() examples as legacy, not as the preferred modern pattern.
  • Keep retries and timeouts separate from the response-handling style decision.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.