Reactive Programming & Resilience Patterns
A practical guide to modern Java concurrency combining reactive design with resilience engineering.
A practical guide to modern Java concurrency (Java 21+), combining reactive-style design with resilience engineering, powered by three modern language features: Records, Pattern Matching, and Virtual Threads.
Table of Contents
- Building Blocks: Records
- Pattern Matching
- Virtual Threads
- Reactive Programming vs. Virtual Threads
- Resilience Patterns
- Full Example: A Resilient Order Service
- Summary Table
- Java Concurrency - All Use Cases & Patterns
1. Building Blocks: Records
Records give us immutable, compact data carriers - perfect for modeling requests, responses, and results in a resilient system.
// A simple immutable data carrier
public record OrderRequest(String orderId, String customerId, BigDecimal amount) {}
public record OrderResult(String orderId, String status, Instant processedAt) {}
Modeling Outcomes with Sealed Interfaces + Records
The real power appears when you combine records with sealed interfaces to model success/failure outcomes explicitly - this is the foundation of resilient, reactive-style code:
public sealed interface Result<T> permits Result.Success, Result.Failure {
record Success<T>(T value) implements Result<T> {}
record Failure<T>(Throwable error, String reason) implements Result<T> {}
}
This lets every operation return a Result<T> instead of throwing, making failure an explicit, matchable value - the same philosophy used in reactive streams (onNext / onError), but expressed with plain Java types.
2. Pattern Matching
Java’s pattern matching (switch expressions, record patterns, guarded patterns) lets us destructure sealed types cleanly - no more instanceof casting chains.
public String handle(Result<OrderResult> result) {
return switch (result) {
case Result.Success<OrderResult> s when s.value().status().equals("PAID") ->
"Order " + s.value().orderId() + " paid successfully";
case Result.Success<OrderResult>(var order) ->
"Order " + order.orderId() + " processed with status " + order.status();
case Result.Failure<OrderResult>(var error, var reason)
when error instanceof TimeoutException ->
"Order timed out: " + reason;
case Result.Failure<OrderResult>(var error, var reason) ->
"Order failed: " + reason;
};
}
Key features used above:
- Record patterns:
Result.Failure<OrderResult>(var error, var reason)destructures directly. - Guarded patterns (
when): add conditional logic inline. - Exhaustiveness: the compiler verifies all sealed cases are handled - no
defaultneeded.
This exhaustiveness check is exactly what makes pattern matching a great fit for resilience logic: you can’t forget to handle a failure branch.
3. Virtual Threads
Virtual Threads (JEP 444, finalized in Java 21) are lightweight threads managed by the JVM rather than the OS. You can create millions of them, and blocking calls (I/O, JDBC, HTTP) no longer waste OS threads.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<OrderResult>> futures = orders.stream()
.map(order -> executor.submit(() -> processOrder(order)))
.toList();
for (Future<OrderResult> future : futures) {
System.out.println(future.get());
}
}
Structured Concurrency (Preview, refined through JDK 21-24)
Structured concurrency treats a group of related tasks running in virtual threads as a single unit of work - errors and cancellations propagate cleanly:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<PaymentResult> payment = scope.fork(() -> chargePayment(order));
Future<InventoryResult> inventory = scope.fork(() -> reserveInventory(order));
scope.join(); // wait for both
scope.throwIfFailed(); // propagate first failure
return new OrderResult(order.orderId(), "CONFIRMED", Instant.now());
}
Why this matters for resilience: instead of juggling callback chains, you write plain, blocking, sequential-looking code that scales because the JVM parks virtual threads cheaply during I/O waits.
4. Reactive Programming vs. Virtual Threads
| Aspect | Reactive (Project Reactor / RxJava) | Virtual Threads |
|---|---|---|
| Programming style | Declarative, chained operators (map, flatMap) | Imperative, blocking-looking code |
| Debugging | Harder (stack traces span async boundaries) | Easier (normal stack traces) |
| Backpressure | Built-in (Flux, Mono) | Must be handled manually (e.g., semaphores) |
| Scalability | High (event-loop based) | High (JVM-scheduled, cheap parking) |
| Learning curve | Steep | Low - looks like normal Java |
In practice: many teams now use virtual threads for I/O-bound services instead of fully reactive stacks, while keeping reactive streams for true streaming/backpressure scenarios (e.g., Kafka consumers, WebSocket streams).
A reactive-style operation, expressed with Mono:
public Mono<Result<OrderResult>> processOrderReactive(OrderRequest request) {
return paymentClient.charge(request)
.map(payment -> (Result<OrderResult>) new Result.Success<>(
new OrderResult(request.orderId(), "PAID", Instant.now())))
.onErrorResume(ex -> Mono.just(new Result.Failure<>(ex, ex.getMessage())))
.timeout(Duration.ofSeconds(3));
}
The same logic, expressed with virtual threads + records + pattern matching:
public Result<OrderResult> processOrderBlocking(OrderRequest request) {
try {
var payment = paymentClient.chargeBlocking(request); // blocks a virtual thread - cheap
return new Result.Success<>(new OrderResult(request.orderId(), "PAID", Instant.now()));
} catch (Exception ex) {
return new Result.Failure<>(ex, ex.getMessage());
}
}
Both are valid - the virtual-thread version is often simpler to read and debug.
5. Resilience Patterns
All patterns below are shown both as hand-rolled implementations (using records + pattern matching + virtual threads) and as Resilience4j equivalents, since Resilience4j is the de-facto standard library for these patterns on the JVM.
5.1 Retry
Retries a failing operation with backoff.
public record RetryPolicy(int maxAttempts, Duration initialDelay, double backoffMultiplier) {
public <T> Result<T> execute(Supplier<Result<T>> operation) {
Duration delay = initialDelay;
Result<T> last = null;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
last = operation.get();
if (last instanceof Result.Success<T>) {
return last;
}
if (attempt < maxAttempts) {
try {
Thread.sleep(delay); // virtual thread - cheap to park
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
delay = delay.multipliedBy((long) backoffMultiplier);
}
}
return last;
}
}
Resilience4j equivalent:
RetryConfig config = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(500))
.build();
Retry retry = Retry.of("orderService", config);
Supplier<OrderResult> decorated = Retry.decorateSupplier(retry, () -> processOrder(request));
5.2 Circuit Breaker
Stops calling a failing dependency after a failure threshold, giving it time to recover.
public final class CircuitBreaker {
public sealed interface State permits State.Closed, State.Open, State.HalfOpen {
record Closed(int failureCount) implements State {}
record Open(Instant openedAt) implements State {}
record HalfOpen() implements State {}
}
private volatile State state = new State.Closed(0);
private final int failureThreshold;
private final Duration openDuration;
public CircuitBreaker(int failureThreshold, Duration openDuration) {
this.failureThreshold = failureThreshold;
this.openDuration = openDuration;
}
public <T> Result<T> execute(Supplier<Result<T>> operation) {
return switch (state) {
case State.Open(var openedAt) when Duration.between(openedAt, Instant.now()).compareTo(openDuration) < 0 ->
new Result.Failure<>(new IllegalStateException("Circuit open"), "circuit-open");
case State.Open ignored -> {
state = new State.HalfOpen();
yield attempt(operation);
}
case State.HalfOpen ignored -> attempt(operation);
case State.Closed(var failures) -> attempt(operation);
};
}
private <T> Result<T> attempt(Supplier<Result<T>> operation) {
Result<T> result = operation.get();
state = switch (result) {
case Result.Success<T> s -> new State.Closed(0);
case Result.Failure<T> f -> {
int failures = (state instanceof State.Closed(var count)) ? count + 1 : failureThreshold;
yield failures >= failureThreshold
? new State.Open(Instant.now())
: new State.Closed(failures);
}
};
return result;
}
}
Resilience4j equivalent:
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(10))
.slidingWindowSize(10)
.build();
CircuitBreaker cb = CircuitBreaker.of("paymentService", config);
Supplier<OrderResult> decorated = CircuitBreaker.decorateSupplier(cb, () -> processOrder(request));
5.3 Timeout
Bounds how long an operation may take - critical when using virtual threads for blocking calls.
public <T> Result<T> withTimeout(Duration timeout, Callable<T> operation) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<T> future = executor.submit(operation);
try {
return new Result.Success<>(future.get(timeout.toMillis(), TimeUnit.MILLISECONDS));
} catch (TimeoutException e) {
future.cancel(true);
return new Result.Failure<>(e, "operation-timed-out");
} catch (Exception e) {
return new Result.Failure<>(e, e.getMessage());
}
}
}
5.4 Bulkhead
Limits concurrent access to a resource so one overloaded dependency can’t exhaust all threads (even cheap virtual threads benefit from bounded concurrency to protect downstream systems).
public record Bulkhead(Semaphore semaphore) {
public static Bulkhead of(int maxConcurrentCalls) {
return new Bulkhead(new Semaphore(maxConcurrentCalls));
}
public <T> Result<T> execute(Supplier<Result<T>> operation) {
if (!semaphore.tryAcquire()) {
return new Result.Failure<>(new RejectedExecutionException(), "bulkhead-full");
}
try {
return operation.get();
} finally {
semaphore.release();
}
}
}
5.5 Rate Limiter
Caps the number of calls per time window.
public final class RateLimiter {
private final int permitsPerWindow;
private final Duration window;
private final AtomicInteger count = new AtomicInteger(0);
private volatile Instant windowStart = Instant.now();
public RateLimiter(int permitsPerWindow, Duration window) {
this.permitsPerWindow = permitsPerWindow;
this.window = window;
}
public synchronized boolean tryAcquire() {
if (Duration.between(windowStart, Instant.now()).compareTo(window) > 0) {
windowStart = Instant.now();
count.set(0);
}
return count.incrementAndGet() <= permitsPerWindow;
}
}
5.6 Fallback
Provides a default response when all else fails - the natural terminal step in a switch over Result.
public OrderResult withFallback(Result<OrderResult> result, OrderRequest request) {
return switch (result) {
case Result.Success<OrderResult>(var order) -> order;
case Result.Failure<OrderResult> f ->
new OrderResult(request.orderId(), "PENDING_MANUAL_REVIEW", Instant.now());
};
}
6. Full Example: A Resilient Order Service
This combines records (data + state modeling), pattern matching (control flow over outcomes), and virtual threads (cheap concurrency for I/O), layering Retry → Circuit Breaker → Timeout → Fallback:
public final class ResilientOrderService {
private final RetryPolicy retryPolicy =
new RetryPolicy(3, Duration.ofMillis(200), 2.0);
private final CircuitBreaker circuitBreaker =
new CircuitBreaker(5, Duration.ofSeconds(30));
private final Bulkhead bulkhead =
Bulkhead.of(50);
private final PaymentClient paymentClient;
public ResilientOrderService(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
public OrderResult processOrder(OrderRequest request) {
Result<OrderResult> result = bulkhead.execute(() ->
circuitBreaker.execute(() ->
retryPolicy.execute(() -> callPaymentWithTimeout(request))
)
);
return switch (result) {
case Result.Success<OrderResult>(var order) -> order;
case Result.Failure<OrderResult>(var error, var reason)
when error instanceof TimeoutException ->
new OrderResult(request.orderId(), "TIMED_OUT_RETRY_LATER", Instant.now());
case Result.Failure<OrderResult> f ->
new OrderResult(request.orderId(), "FAILED: " + f.reason(), Instant.now());
};
}
private Result<OrderResult> callPaymentWithTimeout(OrderRequest request) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<OrderResult> future = executor.submit(() -> {
paymentClient.chargeBlocking(request);
return new OrderResult(request.orderId(), "PAID", Instant.now());
});
return new Result.Success<>(future.get(3, TimeUnit.SECONDS));
} catch (TimeoutException e) {
return new Result.Failure<>(e, "payment-timeout");
} catch (Exception e) {
return new Result.Failure<>(e, e.getMessage());
}
}
public List<OrderResult> processOrdersConcurrently(List<OrderRequest> requests) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<OrderResult>> futures = requests.stream()
.map(req -> executor.submit(() -> processOrder(req)))
.toList();
return futures.stream()
.map(f -> {
try {
return f.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.toList();
}
}
}
Each of the 1,000 concurrent orders in processOrdersConcurrently gets its own cheap virtual thread; each one independently passes through bulkhead → circuit breaker → retry → timeout, with the final status resolved through exhaustive pattern matching on the sealed Result type.
7. Summary Table
| Concept | Purpose | Where it appears above |
|---|---|---|
| Record | Immutable data / state carrier | OrderRequest, OrderResult, Result.Success/Failure, CircuitBreaker.State |
| Sealed interface | Exhaustive, closed set of outcomes | Result<T>, CircuitBreaker.State |
Pattern matching (switch) | Exhaustive, safe branching over outcomes | Every handle(...)/processOrder(...) method |
| Record pattern | Destructuring nested data in one step | Result.Failure<OrderResult>(var error, var reason) |
Guarded pattern (when) | Conditional matching | Result.Failure<T> when error instanceof TimeoutException |
| Virtual thread | Cheap concurrency for blocking I/O | Executors.newVirtualThreadPerTaskExecutor() |
| Structured concurrency | Safe, scoped parallel tasks | StructuredTaskScope.ShutdownOnFailure |
| Retry | Recover from transient failures | RetryPolicy |
| Circuit Breaker | Stop calling a failing dependency | CircuitBreaker |
| Timeout | Bound operation duration | withTimeout |
| Bulkhead | Isolate/limit concurrent load | Bulkhead |
| Rate Limiter | Cap throughput | RateLimiter |
| Fallback | Degrade gracefully | withFallback |
8. Java Concurrency - All Use Cases & Patterns
A companion reference of Java concurrency patterns, organized from the most fundamental primitives up to distributed-systems coordination. Examples target JDK 25 (Records, Pattern Matching, Virtual Threads, Structured Concurrency); a few APIs are still preview and require --enable-preview.
8.1 Basic Concurrency Primitives
- Thread / Runnable - the most basic thread creation.
- synchronized / ReentrantLock - critical-section protection.
- ReadWriteLock - many-reader / single-writer scenarios.
- Semaphore - limit the number of concurrent accesses.
- CountDownLatch - wait for N tasks to finish.
- CyclicBarrier - have multiple threads meet at the same point.
- Atomic classes (
AtomicInteger,AtomicReference, CAS) - lock-free counter/state updates. - ThreadLocal - thread-local state (use with care in Virtual Threads - see
ScopedValue). - ScopedValue (preview) - virtual-thread-friendly alternative to
ThreadLocal.
// Thread / Runnable - platform vs virtual (JDK 21+)
Thread.ofPlatform().start(() -> System.out.println("platform"));
Thread.ofVirtual().start(() -> System.out.println("virtual")); // cheap, I/O-friendly
// synchronized vs ReentrantLock
synchronized (lock) { counter++; }
lock.lock();
try { counter++; } finally { lock.unlock(); }
// ReadWriteLock - many readers, one writer
var rw = new ReentrantReadWriteLock();
rw.readLock().lock(); try { var v = cache; } finally { rw.readLock().unlock(); }
rw.writeLock().lock(); try { cache = v; } finally { rw.writeLock().unlock(); }
// Semaphore - cap concurrent access
var sem = new Semaphore(5);
if (sem.tryAcquire()) { try { work(); } finally { sem.release(); } }
// CountDownLatch - wait for N tasks
var latch = new CountDownLatch(3);
jobs.forEach(j -> executor.submit(() -> { j.run(); latch.countDown(); }));
latch.await();
// CyclicBarrier - threads meet at a common point
var barrier = new CyclicBarrier(4, () -> System.out.println("all arrived"));
parties.forEach(p -> executor.submit(() -> { phase1(); barrier.await(); phase2(); }));
// Atomic classes (CAS) - lock-free counters/state
var count = new AtomicInteger();
count.incrementAndGet();
var ref = new AtomicReference<String>("init");
ref.compareAndSet("init", "updated");
// ThreadLocal
var tl = ThreadLocal.withInitial(() -> "ctx");
String ctx = tl.get();
// ScopedValue (preview) - virtual-thread friendly, structured sharing
ScopedValue.where(USER_ID, "u-1").run(() -> handleRequest());
8.2 Executor / Thread Pool Patterns
- Fixed Thread Pool - a fixed number of workers.
- Cached Thread Pool - a pool that grows and shrinks on demand.
- Scheduled Executor - periodic / delayed tasks.
- Work-Stealing Pool (
ForkJoinPool) - an idle thread “steals” another thread’s work. - Fork/Join (Divide and Conquer) - split a large task into recursive parts.
- Thread Pool Per Resource - a separate pool per dependency (related to bulkhead).
- Virtual Thread Per Task Executor - a separate, cheap virtual thread per task.
// Fixed Thread Pool
try (var pool = Executors.newFixedThreadPool(8)) { pool.submit(task); }
// Cached Thread Pool
try (var pool = Executors.newCachedThreadPool()) { pool.submit(task); }
// Scheduled Executor - delayed / periodic
try (var sched = Executors.newScheduledThreadPool(2)) {
sched.schedule(task, 5, TimeUnit.SECONDS);
sched.scheduleAtFixedRate(task, 0, 1, TimeUnit.MINUTES);
}
// Work-Stealing Pool
try (var fjp = Executors.newWorkStealingPool()) { fjp.submit(task); }
// Fork/Join - divide and conquer
class SumTask extends RecursiveTask<Long> {
protected Long compute() {
if (range.size() <= THRESHOLD) return range.sum();
var left = new SumTask(range.left()).fork(); // run async
long right = new SumTask(range.right()).compute(); // run inline
return left.join() + right;
}
}
// Thread Pool Per Resource - isolate downstream dependencies (bulkhead)
var paymentPool = Executors.newFixedThreadPool(4);
var inventoryPool = Executors.newFixedThreadPool(4);
// Virtual Thread Per Task Executor - millions of cheap tasks
try (var vt = Executors.newVirtualThreadPerTaskExecutor()) {
orders.forEach(o -> vt.submit(() -> process(o)));
}
8.3 Structured Concurrency (the Virtual Thread era)
StructuredTaskScope.ShutdownOnFailure- cancel all subtasks if one fails.StructuredTaskScope.ShutdownOnSuccess- take the first successful result, cancel the rest.- Fan-out / Fan-in - split a request into parallel subtasks and merge the results.
- Scatter-Gather - query multiple services in parallel and collect the best/first response.
// ShutdownOnFailure - all-or-nothing
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var a = scope.fork(() -> fetchA());
var b = scope.fork(() -> fetchB());
scope.join(); // wait for both
scope.throwIfFailed(); // propagate the first failure
return new Result(a.get(), b.get());
}
// ShutdownOnSuccess - first success wins, siblings cancelled
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
scope.fork(() -> fetchFromCache());
scope.fork(() -> fetchFromDb());
scope.join();
return scope.result(); // first successful result
}
// Fan-out / Fan-in - parallel map, then collect
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
List<Supplier<Item>> tasks = ids.stream()
.map(id -> scope.fork(() -> load(id)))
.map(StructuredTaskScope.Subtask::get)
.toList();
scope.join();
return tasks.stream().map(Supplier::get).toList(); // fan-in
}
// Scatter-Gather - parallel query, take the first/best reply
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<Quote>()) {
providers.forEach(p -> scope.fork(p::quote));
scope.join();
return scope.result();
}
8.4 Async / Future-Based Patterns
- Future / Promise - a representation of an operation whose result will be ready later.
CompletableFuturechaining (thenApply,thenCompose,thenCombine) - async/await-like composition.- Callback Pattern - a function triggered when an operation completes.
- Parallel Streams (
stream().parallel()) - automatic parallelism over data collections.
// Future / Promise
Future<Order> f = executor.submit(() -> loadOrder(id));
Order order = f.get();
// CompletableFuture chaining
CompletableFuture<Order> cf = CompletableFuture
.supplyAsync(() -> loadOrder(id))
.thenApply(Order::total) // map
.thenCompose(this::charge) // flatMap
.thenCombine(loadShipping(), Receipt::new); // zip two futures
// Callback - react on completion
cf.whenComplete((result, err) ->
System.out.println(err == null ? result : err.getMessage()));
// Parallel Streams
List<Result> results = orders.parallelStream()
.map(this::process)
.toList();
8.5 Reactive / Event-Driven Patterns
- Reactive Streams (
Flux/Mono,Observable) - push-based, backpressure-aware streams. - Event Loop - event-driven I/O on a single thread (Netty, Node.js style).
- Publish-Subscribe - loosely coupled communication via publisher/subscriber.
- Backpressure strategies (buffer, drop, latest, error) - prevent the producer from overwhelming the consumer.
- Pipeline Pattern - data flows through successive transformation stages (each stage may run on a separate thread).
// Reactive Streams (Project Reactor)
Mono<Order> order = client.getOrder(id);
Flux<OrderEvent> events = order
.flatMapMany(o -> eventClient.stream(o.id()))
.onBackpressureBuffer(100);
// Event Loop - a single event-loop thread drives non-blocking I/O
var group = new NioEventLoopGroup(1); // one event-loop thread
group.register(channel);
// Publish-Subscribe
publisher.subscribe(subscriber); // one publisher, many subscribers
// Backpressure strategies
flux.onBackpressureBuffer(100); // buffer
flux.onBackpressureDrop(); // drop newest when full
flux.onBackpressureLatest(); // keep only the latest
flux.onBackpressureError(); // fail fast
// Pipeline Pattern - successive transformation stages
stream.map(Parser::parse)
.map(Validator::validate)
.map(Enricher::enrich)
.forEach(sink);
8.6 Producer-Consumer & Coordination Patterns
- Producer-Consumer (
BlockingQueue) - a buffer between producer and consumer. - Worker Pool / Task Queue - a fixed number of workers process jobs from a queue.
- Leader-Follower Pattern - threads take turns assuming the leader role to process events.
- Guarded Suspension - keep a thread waiting until a condition is met.
- Balking Pattern - reject an operation immediately if the object isn’t in a suitable state.
- Two-Phase Termination - a protocol for safely shutting down a thread.
- Double-Checked Locking - performant thread-safety in lazy initialization.
// Producer-Consumer (BlockingQueue)
var queue = new LinkedBlockingQueue<Item>(100);
queue.put(item); // producer blocks when full
Item item = queue.take(); // consumer blocks when empty
// Worker Pool / Task Queue
try (var pool = Executors.newFixedThreadPool(4)) {
jobs.forEach(pool::submit);
}
// Guarded Suspension - wait until the condition holds
synchronized (lock) {
while (!ready) lock.wait();
use();
}
// Balking - reject immediately if not in a suitable state
synchronized (lock) {
if (busy) return; // balk
busy = true;
}
// Two-Phase Termination
t.interrupt(); // phase 1: request shutdown
while (!Thread.currentThread().isInterrupted()) // phase 2: observe & clean up
work();
// Double-Checked Locking
class Singleton {
private volatile Singleton instance;
Singleton get() {
if (instance == null) {
synchronized (this) {
if (instance == null) instance = new Singleton();
}
}
return instance;
}
}
8.7 Patterns Intersecting with Resilience
Detailed with full code in §5; listed here because they overlap directly with concurrency.
- Retry (with backoff)
- Circuit Breaker
- Timeout
- Bulkhead (semaphore / thread-pool isolation)
- Rate Limiter (token bucket, leaky bucket, sliding window)
- Fallback
8.8 Concurrency Patterns in Distributed Systems
- Saga Pattern - run distributed transactions step by step, with compensation.
- Idempotency Key - prevent the same request from being processed more than once.
- Optimistic Locking / Version Control - detect conflicting updates via a version number.
- Distributed Lock (Redis/ZooKeeper-based) - mutual exclusion across multiple instances.
// Saga - local steps with compensating actions on failure
try {
reserveInventory(); chargePayment(); createShipment();
} catch (PaymentException e) {
releaseInventory(); // compensate
}
// Idempotency Key - replay the stored result, don't re-execute
var existing = repo.find(key);
if (existing != null) return existing;
var result = execute();
repo.save(key, result);
// Optimistic Locking - version check; 0 rows updated => conflict, retry
// UPDATE orders SET total=?, version=version+1 WHERE id=? AND version=?
// Distributed Lock (Redis) - mutual exclusion across instances
boolean locked = redis.setIfAbsent("lock:order-1", token, 30, SECONDS);
if (locked) { try { work(); } finally { redis.release("lock:order-1", token); } }
9. Further Reading
- JEP 444 - Virtual Threads (Java 21)
- JEP 441 - Pattern Matching for switch (Java 21)
- JEP 395 - Records (Java 16)
- Resilience4j documentation
- Project Reactor documentation (for true streaming/backpressure use cases)