eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – Spring Sale 2026 – NPI EA (cat= Baeldung)
announcement - icon

Yes, we're now running our Spring Sale. All Courses are 30% off until 31st March, 2026

>> EXPLORE ACCESS NOW

Partner – Diagrid – NPI EA (cat= Testing)
announcement - icon

In distributed systems, managing multi-step processes (e.g., validating a driver, calculating fares, notifying users) can be difficult. We need to manage state, scattered retry logic, and maintain context when services fail.

Dapr Workflows solves this via Durable Execution which includes automatic state persistence, replaying workflows after failures and built-in resilience through retries, timeouts and error handling.

In this tutorial, we'll see how to orchestrate a multi-step flow for a ride-hailing application by integrating Dapr Workflows and Spring Boot:

>> Dapr Workflows With PubSub

Course – Spring Sale 2026 – NPI (cat=Baeldung)
announcement - icon

Yes, we're now running our Spring Sale. All Courses are 30% off until 31st March, 2026

>> EXPLORE ACCESS NOW

1. Overview

In this article, we’ll focus on understanding the switchIfEmpty() operator in Spring Reactive and its behavior with and without the defer() operator. We’ll explore how these operators interact in different scenarios, providing practical examples to illustrate their impact on reactive streams.

2. Use of switchIfEmpty() and Defer()

The switchIfEmpty() is an operator in Mono and Flux that executes an alternate producer flow if the source producer is empty. If the primary source publisher emits no data, this operator switches to data emissions from an alternate source.

Let’s consider an endpoint that retrieves user details by ID from a large file. Each time someone requests user details from the file, iterating through the file consumes a lot of time. Thus, it makes more sense for frequently accessed IDs to have their details cached instead.

When the endpoint receives a request, we’ll first search the cache. If the user details are available, we’ll return the response. If not, we’ll fetch the data from the file and cache it for subsequent requests.

In this context, the primary data provider is the flow that checks for the key’s existence in the cache, while the alternative is the flow that checks the key in the file and updates the cache. The switchIfEmpty() operator can efficiently switch the source provider based on the availability of data in the cache.

It’s also important to understand the use of the defer() operator, which defers or delays the evaluation of a function until a subscription occurs. When we do not use the defer() operator with switchIfEmpty(), the expression evaluates immediately (eagerly), potentially causing unexpected side effects.

3. Setup

Let’s explore the example further to understand the behavior of the switchIfEmpty() operator under different circumstances.

We’ll implement the necessary code and analyze the system logs to determine whether we fetch the user from the cache or the file.

3.1. Data Model

First, let’s define a user model that contains a few details like id, name, email, roles:

public class User {

    @JsonProperty("id")
    private String id;

    @JsonProperty("email")
    private String email;

    @JsonProperty("username")
    private String username;

    @JsonProperty("roles")
    private String roles;

    // standard getters and setters...
}

3.2. User Data Setup

Subsequently, let’s maintain a file in the classpath(users.json) that contains all the user details in a JSON format:

[  
  {
    "id": "66b296723881ea345705baf1",
    "email": "[email protected]",
    "username": "reid90",
    "roles": "member"
  },
  {
    "id": "66b29672e6f99a7156cc4ada",
    "email": "[email protected]",
    "username": "boyle94",
    "roles": "admin"
  },
...
]

3.3. Controller and Service Implementation

In the following step, let’s add a controller that retrieves user details by ID. It will accept an optional boolean parameter withDefer, and involve different implementations based on this query parameter:

@GetMapping("/user/{id}")
public Mono<ResponseEntity<User>> findUserDetails(@PathVariable("id") String id, 
  @RequestParam("withDefer") boolean withDefer) {
    return (withDefer ? userService.findByUserIdWithDefer(id) : 
      userService.findByUserIdWithoutDefer(id)).map(ResponseEntity::ok);
}

Then, let’s define these two implementations in UserService, with and without defer(), to understand the behavior of switchIfEmpty():

public Mono<User> findByUserIdWithDefer(String id) {
    return fetchFromCache(id).switchIfEmpty(Mono.defer(() -> fetchFromFile(id)));
}
public Mono<User> findByUserIdWithoutDefer(String id) {
    return fetchFromCache(id).switchIfEmpty(fetchFromFile(id));
}

For simplicity, let’s implement an in-memory cache to retain user information as the primary data provider for requests. We’ll also log each access, which allows us to determine whether the cache retrieved the data:

private final Map<String, User> usersCache;

private Mono<User> fetchFromCache(String id) {
    User user = usersCache.get(id);
    if (user != null) {
        LOG.info("Fetched user {} from cache", id);
        return Mono.just(user);
    }
    return Mono.empty();
}

Following that, let’s fetch user details from the file when the ID is not in the cache and update the cache if the data is found:

private Mono<User> fetchFromFile(String id) {
    try {
        File file = new ClassPathResource("users.json").getFile();
        String usersData = new String(Files.readAllBytes(file.toPath()));
        List<User> users = objectMapper.readValue(usersData, new TypeReference<List<User>>() {
        });
        User user = users.stream()
          .filter(u -> u.getId()
            .equalsIgnoreCase(id))
          .findFirst()
          .get();
        usersCache.put(user.getId(), user);
        LOG.info("Fetched user {} from file", id);
        return Mono.just(user);
    } catch (IOException e) {
        return Mono.error(e);
    }
}

Note the logging details to assert if user data was retrieved from the file.

4. Testing

Let’s add a ListAppender in the @BeforeEach test method to track the logs. We’ll use it to determine whether the cache or the file executes the function for different requests:

protected ListAppender<ILoggingEvent> listAppender;

@BeforeEach
void setLogger() {
    Logger logger = (Logger) LoggerFactory.getLogger(UserService.class);
    logger.setLevel(Level.DEBUG);
    listAppender = new ListAppender<>();
    logger.addAppender(listAppender);
    listAppender.start();
}

We can add some tests to validate various conditions in the following sections.

4.1. switchIfEmpty() With defer() on Non-Empty Source

We’ll verify that the implementation retrieves user data from the cache only when the request has the withDefer parameter set to true and we’ll assert the logger output accordingly:

@Test
void givenUserDataIsAvailableInCache_whenUserByIdIsRequestedWithDeferParameter_thenCachedResponseShouldBeRetrieved() {
    usersCache = new HashMap<>();
    User cachedUser = new User("66b29672e6f99a7156cc4ada", "[email protected]", "boyle94", "admin");
    usersCache.put("66b29672e6f99a7156cc4ada", cachedUser);
    userService.getUsers()
      .putAll(usersCache);

    webTestClient.get()
      .uri("/api/v1/user/66b29672e6f99a7156cc4ada?withDefer=true")
      .exchange()
      .expectStatus()
      .isOk()
      .expectBody(String.class)
      .isEqualTo("{\"id\":\"66b29672e6f99a7156cc4ada\"," +
        "\"email\":\"[email protected]\",\"username\":\"boyle94\",\"roles\":\"admin\"}");

    assertTrue(listAppender.list.stream()
      .anyMatch(e -> e.toString()
        .contains("Fetched user 66b29672e6f99a7156cc4ada from cache")));

    assertTrue(listAppender.list.stream()
      .noneMatch(e -> e.toString()
        .contains("Fetched user 66b29672e6f99a7156cc4ada from file")));
}

When we use switchIfEmpty() with the defer() operator, the alternative source provider doesn’t get evaluated eagerly.

4.2. switchIfEmpty() Without defer() on Non-Empty Source

Let’s add another test to check the behavior of using switchIfEmpty() without the defer() operator:

@Test
void givenUserDataIsAvailableInCache_whenUserByIdIsRequestedWithoutDeferParameter_thenUserIsFetchedFromFileInAdditionToCache() {
    usersCache = new HashMap<>();
    User cachedUser1 = new User("66b29672e6f99a7156cc4ada", "[email protected]", "boyle94", "admin");
    usersCache.put("66b29672e6f99a7156cc4ada", cachedUser1);
    userService.getUsers().putAll(usersCache);

    webTestClient.get()
      .uri("/api/v1/user/66b29672e6f99a7156cc4ada?withDefer=false")
      .exchange()
      .expectStatus()
      .isOk()
      .expectBody(String.class)
      .isEqualTo("{\"id\":\"66b29672e6f99a7156cc4ada\"," +
        "\"email\":\"[email protected]\",\"username\":\"boyle94\",\"roles\":\"admin\"}");

    assertTrue(listAppender.list.stream()
      .anyMatch(e -> e.toString()
        .contains("Fetched user 66b29672e6f99a7156cc4ada from file")));

    assertTrue(listAppender.list.stream()
      .anyMatch(e -> e.toString()
        .contains("Fetched user 66b29672e6f99a7156cc4ada from cache")));
}

As we can see, the implementation fetched the user details from both the cache and the file, but it ultimately served the response from the cache. The code block in the alternate source was triggered unnecessarily, despite emissions from the primary source (cache).

4.3. switchIfEmpty() With defer() on Empty Source

Following that, let’s add a test to verify that user details are retrieved from the file when there is no data in the cache, specifically when using the defer() operator:

@Test
void givenUserDataIsNotAvailableInCache_whenUserByIdIsRequestedWithDeferParameter_thenFileResponseShouldBeRetrieved() {
    webTestClient.get()
      .uri("/api/v1/user/66b29672e6f99a7156cc4ada?withDefer=true")
      .exchange()
      .expectStatus()
      .isOk()
      .expectBody(String.class)
      .isEqualTo("{\"id\":\"66b29672e6f99a7156cc4ada\"
        ,\"email\":\"[email protected]\",\"username\":\"boyle94\",\"roles\":\"admin\"}");

    assertTrue(listAppender.list.stream()
      .anyMatch(e -> e.toString()
        .contains("Fetched user 66b29672e6f99a7156cc4ada from file")));

    assertTrue(listAppender.list.stream()
      .noneMatch(e -> e.toString()
        .contains("Fetched user 66b29672e6f99a7156cc4ada from cache")));
}

The API fetches the user details from the file as intended, rather than attempting to retrieve them from the cache.

4.4. switchIfEmpty() Without defer() on Empty Source

Finally, let’s add a test to validate that the implementation retrieves user details from the file (i.e., no data in the cache) when used without defer():

@Test
void givenUserDataIsNotAvailableInCache_whenUserByIdIsRequestedWithoutDeferParameter_thenFileResponseShouldBeRetrieved() {
    webTestClient.get()
      .uri("/api/v1/user/66b29672e6f99a7156cc4ada?withDefer=false")
      .exchange()
      .expectStatus()
      .isOk()
      .expectBody(String.class)
      .isEqualTo("{\"id\":\"66b29672e6f99a7156cc4ada\"," + "\"email\":\"[email protected]\",\"username\":\"boyle94\",\"roles\":\"admin\"}");

    assertTrue(listAppender.list.stream()
      .anyMatch(e -> e.toString()
        .contains("Fetched user 66b29672e6f99a7156cc4ada from file")));

    assertTrue(listAppender.list.stream()
      .noneMatch(e -> e.toString()
        .contains("Fetched user 66b29672e6f99a7156cc4ada from cache")));
}

As there is no data in the cache, the user details are not fetched from the cache but will be attempted as a side-effect but still the API fetches the user details from the file as intended.

5. Conclusion

In this article, we focussed on understanding the switchIfEmpty() operator in Spring Reactive and its various behaviors through tests.

Using switchIfEmpty() in conjunction with defer() ensures access to the alternative data source only when necessary. This prevents unnecessary computations and potential side effects.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

Course – Spring Sale 2026 – NPI EA (cat= Baeldung)
announcement - icon

Yes, we're now running our Spring Sale. All Courses are 30% off until 31st March, 2026

>> EXPLORE ACCESS NOW

Course – Spring Sale 2026 – NPI (All)
announcement - icon

Yes, we're now running our Spring Sale. All Courses are 30% off until 31st March, 2026

>> EXPLORE ACCESS NOW

eBook Jackson – NPI EA – 3 (cat = Jackson)