Note
This article uses Java 21 preview structured concurrency APIs (JEP 453). See Part 9 for migration changes in Java 25 preview APIs. Compile and run with --enable-preview.
Why Timeout Alone Is Not Enough
Timeouts handle slow dependencies, but many production incidents are business-condition failures:
- payment authorization fails,
- risk checks fail,
- dependency breaker opens.
In these cases, continuing sibling work is wasted load. Conditional cancellation addresses this.
Pattern 1: Business-Condition Fail-Fast
In Java 21 preview, a practical pattern is to convert terminal business failures into exceptions inside subtasks so ShutdownOnFailure can cancel siblings.
public String circuitBreakerExample() throws Exception {
if (circuitBreakerFailures.get() > 3) {
logger.warn("Circuit breaker is OPEN - failing fast");
return "Circuit breaker is OPEN - service unavailable";
}
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var primaryService = scope.fork(() -> {
if (Math.random() < 0.3) {
circuitBreakerFailures.incrementAndGet();
throw new RuntimeException("Service failure");
}
circuitBreakerFailures.set(0);
return simulateServiceCall("primary-service", 100);
});
scope.join();
scope.throwIfFailed();
String result = "Circuit Breaker Result: " + primaryService.get();
logger.info("Circuit breaker example completed successfully");
return result;
}
}
This avoids burning downstream capacity after known terminal failures.
Pattern 2: Circuit Breaker + Structured Scope
Circuit breakers decide whether a call should even be attempted. Structured scopes then coordinate lifecycle for calls that do proceed.
public String callProtectedService(String request) throws Exception {
if (dbCircuitBreaker.isOpen()) {
String message = String.format(
"Circuit breaker is OPEN for %s - failing fast (failures: %d/%d, next retry in: %s)",
dbCircuitBreaker.getServiceName(),
dbCircuitBreaker.getFailureCount(),
dbCircuitBreaker.getThreshold(),
dbCircuitBreaker.getTimeUntilRetry());
logger.warn(message);
throw new RuntimeException(message);
}
try {
String result = scopedHandler.runInScope(() -> callUnreliableService(request));
dbCircuitBreaker.onSuccess();
logger.info("Protected service call succeeded, circuit breaker reset");
return result;
} catch (Exception e) {
dbCircuitBreaker.onFailure();
logger.error("Protected service call failed (failure {}/{}): {}",
dbCircuitBreaker.getFailureCount(),
dbCircuitBreaker.getThreshold(),
e.getMessage());
throw e;
}
}
Keep breaker policy outside scope internals. Scope handles task lifecycle; breaker handles dependency admission policy.
Record failure only on actual attempts; pre-breaker rejections should usually be tracked as a separate metric.
For production systems, prefer libraries like Resilience4j for persisted breaker state and richer metrics; this example is illustrative.
Pattern 3: Conditional Fallback Scope
For non-critical enrichments, branch into a fallback path instead of failing the whole request.
public <T> T runWithFallback(Callable<T> primary, Callable<T> fallback) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var primaryFuture = scope.fork(primary);
scope.join();
try {
scope.throwIfFailed();
return primaryFuture.get();
} catch (Exception e) {
logger.warn("Primary task failed, using fallback: {}", e.getMessage());
return fallback.call();
}
}
}
Java 21 Preview Limitations
-
ShutdownOnFailure uses exception semantics; if a business condition should fail fast, convert it into an explicit exception.
-
Avoid hidden retry loops in subtasks; retries can multiply traffic during outages, so keep retry policy bounded and observable.
-
Always check throwIfFailed() after join() in Java 21 preview style.
-
Keep side effects idempotent where cancellation can occur mid-flow.
-
Scope shutdown does not interrupt in-flight I/O instantly; virtual threads park gracefully, but long operations may still complete.
-
Pre-breaker rejections should be metered separately from attempt failures.
Testing Guidance
For conditional cancellation flows, test:
- Primary success path.
- Terminal business failure (for example payment declined).
- Dependency timeout + breaker transition.
- Fallback path correctness.
- Idempotency under retries/cancellation.
Build and Runtime Reminder
javac --release 21 --enable-preview ...
java --enable-preview ...
Resources