🕐 Reading time: 3 minutes

In the AML project we're currently assigned to, good old Mario had to improve the performance of some services, avoiding the fetching of data that didn't change frequently. While exploring the WebFlux documentation, he found the .cache() and .share() methods, which lend themselves nicely to a new article!

📌 Cold Flux and why you need .cache()

In Reactor, Fluxes are cold: every time a subscriber subscribes, the flow starts over from scratch, re-running the logic at the source. This is the standard behaviour, but not always the desirable one. Imagine, for example, several subscribers that, within a few minutes of each other, query the same expensive source (e.g. a configuration table that updates every 10 minutes). In these cases, re-running everything each time is inefficient. The .cache() method lets you store the data emitted by the flow on the first subscription, and reuse it for the following ones, thus avoiding recomputation.

📑 The experiment – with and without cache

class SampleTest017 {

    @Test
    void test_buildFlux_noCache() {
        final AtomicInteger subscriptionCount = new AtomicInteger();
        final Flux<String> sharedFlux = buildFlux(subscriptionCount);
        final List<String> result1 = invokeAndCollect(sharedFlux);
        final List<String> result2 = invokeAndCollect(sharedFlux);
        checkResults(result1, result2);
        //expect <2> because the source is cold and not cached
        assertEquals(2, subscriptionCount.get());
    }

    @Test
    void test_buildFlux_withCache() {
        final AtomicInteger subscriptionCount = new AtomicInteger();
        final Flux<String> sharedFlux = buildFlux(subscriptionCount)
                .cache(); //cache the emitted items for all subsequent subscribers
        final List<String> result1 = invokeAndCollect(sharedFlux); //1st subscription triggers the source
        final List<String> result2 = invokeAndCollect(sharedFlux); //2nd subscription uses the cached data
        checkResults(result1, result2);
        //expect <1> because the result is cached after the first subscription
        assertEquals(1, subscriptionCount.get());
    }

    //build a Flux that emits "A", "B", "C" and increments the count each time the Flux is subscribed to
    private Flux<String> buildFlux(final AtomicInteger count) {
        return Flux.just("A", "B", "C").doOnSubscribe(subscription -> count.incrementAndGet());
    }

    private List<String> invokeAndCollect(final Flux<String> sharedFlux) {
        return sharedFlux.collectList().block();
    }

    @SafeVarargs
    private void checkResults(final List<String>... results) {
        Arrays.stream(results).forEach(result -> assertEquals(List.of("A", "B", "C"), result));
    }
}

📑 Analysis of the 1st test – test_buildFlux_noCache()

The default behaviour is clearly visible:

📑 Analysis of the 2nd test – test_buildFlux_withCache()

Here we use .cache():

📌 When is it worth using cache?

⚠️ Limits and things to watch out for

This article is getting way too long… and after all, the rubric is called "Pills"! I'll stop here and leave the .share() method for part 2 – stay tuned!

See you at the next pill! ☕