Was this article helpful?
to mark as helpful
Enjoyed this article?
Get more engineering insights delivered weekly.
Comments
to join the discussion
to mark as helpful
Get more engineering insights delivered weekly.
to join the discussion
Jagdish Salgotra
Apr 6, 2026 · 10 min read · Structured Concurrency
Build resilient Java services using conditional cancellation and circuit breaker patterns. Learn to fail fast, stop early, and implement sophisticated fault-tolerance mechanisms with StructuredTaskScope to handle partial failures gracefully.
Your article assistant
Ask me anything about this article. I'll provide answers with relevant sources.
Try asking:
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.
Timeouts handle slow dependencies, but many production incidents are business-condition failures:
In these cases, continuing sibling work is wasted load. Conditional cancellation addresses this.
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(); // Standard Java 21 preview fail-fast check after join.
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.
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.
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();
}
}
}
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.
For conditional cancellation flows, test:
javac --release 21 --enable-preview ...
java --enable-preview ...