Skip to main content
reactive microservices architecture transactional outbox and event sourcing with java 21

Rehydrating State with Virtual Threads

3 min read Chapter 6 of 10
Summary

Virtual threads improve rehydration performance with StructuredTaskScope in...

Virtual threads improve rehydration performance with StructuredTaskScope in Java 21.

Rehydrating State with Virtual Threads

Introduction

Rehydrating the state of an aggregate root in event sourcing involves reconstructing its current state by replaying historical events from an event store or loading the latest snapshot. This process can be complex and time-consuming, especially when dealing with a large number of events. However, with the introduction of virtual threads in Java, we can significantly improve the performance of this process.

Virtual Threads and Project Loom

Virtual threads, introduced in Project Loom, are a lightweight, user-mode implementation of java.lang.Thread that is managed by the Java runtime rather than the OS. This allows for millions of concurrent instances, making them ideal for I/O-bound tasks such as rehydrating state from an event store or loading snapshots from a database. By leveraging virtual threads, we can increase concurrency throughput and reduce the overhead of creating and context-switching between threads compared to traditional platform threads.

StructuredTaskScope for Rehydration

To coordinate the rehydration process, we can utilize StructuredTaskScope, an API introduced in Java 21 as part of JEP 453 (Structured Concurrency). This API facilitates the coordination of multiple concurrent subtasks by treating them as a single unit of work. By using StructuredTaskScope.ShutdownOnFailure(), we can capture the first exception occurring in subtasks and stop other subtasks, ensuring that the rehydration process is atomic and consistent.

Implementation Example

The following Java code snippet demonstrates how to implement parallel snapshot and event loading using StructuredTaskScope for rehydration:

public Aggregate rehydrate(String aggregateId) throws Exception {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Subtask<Snapshot> snapshotTask = scope.fork(() -> snapshotRepository.findById(aggregateId));
        Subtask<List<Event>> eventsTask = scope.fork(() -> eventStore.findEventsAfter(aggregateId, lastSnapshotVersion));

        scope.join();
        scope.throwIfFailed();

        Snapshot snapshot = snapshotTask.get();
        List<Event> events = eventsTask.get();
        return Aggregate.apply(snapshot, events);
    }
}

Performance Comparison

A comparative analysis of virtual threads versus platform threads for rehydration tasks reveals significant performance differences. The table below summarizes key metrics:

MetricPlatform Threads (Cached Pool)Virtual Threads (Loom)
Max Concurrent Rehydrations~2,000100,000+
Context Switch OverheadHigh (Kernel level)Low (User level)
Memory Per Thread~1 MB (Stack)~1 KB (Heap)
CPU Usage during I/O WaitModerate (Context switching)Near Zero
As shown, virtual threads outperform platform threads in terms of concurrency, context switching overhead, memory usage, and CPU utilization during I/O wait.

Conclusion

By leveraging virtual threads and StructuredTaskScope, we can significantly improve the performance and concurrency of the rehydration process in event sourcing. This approach enables us to maximize throughput during event playback, making it an attractive solution for high-performance applications.

Sources

[1] https://www.kloia.com/blog/benchmarking-java-virtual-threads-a-comprehensive-analysis [2] https://spring.io/blog/2023/02/27/web-applications-and-project-loom/