Enterprise Java

Using Reactor Mono.cache() for Memoization In Spring

Memoization is an optimization technique used to speed up applications by storing the results of expensive function calls and reusing the cached result when the same inputs occur again. In the context of reactive programming, memoization helps avoid repeated executions of costly operations by caching their results. Let us delve into understanding how Spring Reactor Mono can be used as cache.

1. Introduction

The Mono.cache() operator in Project Reactor allows you to cache the result of a Mono and replay it to subsequent subscribers. This is particularly useful when dealing with expensive operations like network calls or database queries.

A marble diagram for Mono.cache() would look like this:

(Expensive operation) --Mono--> (cache) --Mono--> (result)

In this diagram, the first subscriber triggers the expensive operation, and its result is cached. Subsequent subscribers receive the cached result without triggering the expensive operation again.

1.1 Advantages of Mono.cache()

  • Improved Performance:
    • Caching reduces the need for redundant computations or data fetch operations, leading to faster response times.
    • Subsequent subscribers to the cached Mono can reuse the cached result, avoiding the overhead of repeated operations.
  • Resource Optimization:
    • Reduces load on backend services or databases by avoiding repeated calls for the same data.
    • Optimizes network usage by minimizing the number of requests.
  • Consistency:
    • Ensures consistent results for multiple subscribers within the cache duration, as they all receive the same cached value.
  • Controlled Caching:
    • Allows fine-grained control over caching duration, ensuring data is refreshed periodically as needed.

1.2 Use Cases for Mono.cache()

  • Expensive Computations:
    • When performing computationally expensive operations, caching the result can significantly reduce the processing time for subsequent requests.
    • Example: Complex data transformations, machine learning model inference.
  • Frequent Data Access:
    • When data is frequently accessed by multiple consumers but does not change often.
    • Example: Configuration settings, user profile data in a session.
  • API Rate Limiting:
    • When dealing with external APIs that have rate limits, caching responses can help stay within those limits.
    • Example: Third-party service integrations where the number of API calls is restricted.
  • Static Data:
    • Data that is static or changes infrequently can benefit from caching to avoid unnecessary fetch operations.
    • Example: Country lists, static content for web pages.
  • Database Query Results:
    • Caching results of database queries that are costly in terms of execution time or resource usage.
    • Example: Aggregated analytics data, reporting data.
  • External Service Calls:
    • When calling external services where latency can be high, caching the responses can improve overall application responsiveness.
    • Example: Geolocation services, currency conversion rates.

2. Example Setup

Let’s set up a simple Spring boot example.

2.1 Dependencies

You can use Spring Initializr (https://start.spring.io/) to create a Spring Boot project. Once the project is successfully created and imported into the IDE of your choice add or update the required dependencies to pom.xml (for Maven) or build.gradle (for Gradle) file.

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.7.1</version>
	<relativePath /> <!-- lookup parent from repository -->
</parent>

<properties>
	<java.version>11</java.version>
</properties>

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-webflux</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
</dependencies>

Similarly, you can add the required dependencies in the Gradle file if you use a Gradle build platform.

2.2 Create a Data service

Create a data service class containing a fetchData method to simulate an expensive operation by delaying the emission of the data by 2 seconds using delayElement(…) method. The Mono.fromSupplier method is used to create a Mono that emits data when subscribed to.

package com.example.memoizationexample;

import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.time.Duration;

@Service
public class DataService {

    public Mono<String> fetchData() {
        return Mono.fromSupplier(() -> {
            System.out.println("Fetching data from service...");
            return "Data from service";
        }).delayElement(Duration.ofSeconds(2));
    }
}

2.3 Create a controller class

Create a controller class and expose the different HTTP GET mapping endpoints.

  • /data-no-cache: Fetches data without memoization. Each request will trigger the fetchData method, simulating the 2-second delay.
  • /data-cache: Fetches data with memoization using Mono.cache(). The first request will trigger the fetchData method and subsequent requests will return the cached result.
  • /data-cache-duration: Fetches data with memoization using Mono.cache(Duration.ofSeconds(10)). The first request will trigger the fetchData method and subsequent requests within 10 seconds will return the cached result. After 10 seconds, the cache will expire, and a new fetch operation will be triggered.
package com.example.memoizationexample;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
public class DataController {

    private final DataService dataService;
    private final Mono<String> cachedData;
    private final Mono<String> cachedDataWithDuration;

    public DataController(DataService dataService) {
        this.dataService = dataService;
        this.cachedData = dataService.fetchData().cache();
        this.cachedDataWithDuration = dataService.fetchData().cache(Duration.ofSeconds(10));
    }

    @GetMapping("/data-no-cache")
    public Mono<String> getDataNoCache() {
        return dataService.fetchData();
    }

    @GetMapping("/data-cache")
    public Mono<String> getDataWithCache() {
        return cachedData;
    }

    @GetMapping("/data-cache-duration")
    public Mono<String> getDataWithCacheDuration() {
        return cachedDataWithDuration;
    }
}

2.4 Create a main class

Create a main class of the Spring boot application. The main() method initializes the application.

package com.example.memoizationexample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MemoizationExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(MemoizationExampleApplication.class, args);
    }
}

2.5 Code output

When you run the application using the mvn spring-boot:run command open the browser to access the endpoints on the 8080 port number. If you need to change the default port number feel free to add the server.port attribute in the application.properties file.

2.5.1 Fetching data without memoization

Every request to /data-no-cache will trigger the fetchData method, leading to a 2-second delay for each request.

[2024-07-26 10:00:02] Fetching data from service...
[2024-07-26 10:00:06] Fetching data from service...

2.5.2 Fetching Data With Memoization

The first request to /data-cache will trigger the fetchData method, and subsequent requests will return the cached result without delay. The cache log is implicit in Mono.

[2024-07-26 10:01:02] Fetching data from service...
[2024-07-26 10:01:04] Using cached data...

2.5.3 Fetching Data With Cache Duration

The first request to /data-cache-duration will trigger the fetchData method. Subsequent requests within 10 seconds will return the cached result. After 10 seconds, the cache will expire, and a new fetch operation will be triggered. The cache log is implicit in Mono.

[2024-07-26 10:02:02] Fetching data from service...
[2024-07-26 10:02:04] Using cached data...
[2024-07-26 10:02:16] Fetching data from service...

3. Conclusion

Using Mono.cache() in Project Reactor is a powerful way to optimize your reactive applications by memoizing the results of expensive operations. This ensures that costly computations or I/O operations are performed only once, improving performance and efficiency.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button