Resilience,
compiled.
Annotate a Java interface with retry, circuit breaker, timeout and friends. At compile time, an annotation processor writes the plain Java that enforces them. No proxies, no reflection, no framework.
- Java 21+
- Works without Spring, CDI or AspectJ
- Sync and
CompletionStage - Apache 2.0
dependencies { implementation 'dev.resilience4cata:resilience4cata-all:0.10.0' annotationProcessor 'dev.resilience4cata:resilience4cata-processor:0.10.0' }
dependencies { implementation("dev.resilience4cata:resilience4cata-all:0.10.0") annotationProcessor("dev.resilience4cata:resilience4cata-processor:0.10.0") }
<dependency> <groupId>dev.resilience4cata</groupId> <artifactId>resilience4cata-all</artifactId> <version>0.10.0</version> </dependency> <!-- in the maven-compiler-plugin <configuration> --> <annotationProcessorPaths> <path> <groupId>dev.resilience4cata</groupId> <artifactId>resilience4cata-processor</artifactId> <version>0.10.0</version> </path> </annotationProcessorPaths>
Version 0.10.0 on Maven Central. resilience4cata-all brings core, the annotations and configuration; the processor only goes on the annotation processor path. Installation guide
PaymentClient.javapublic interface PaymentClient { @Retry(maxAttempts = 3, delay = "200ms") @CircuitBreaker @Timeout("2s") Receipt charge(Order order); } // once, at startup ResilienceRegistry registry = ResilienceRegistry.builder().build(); PaymentClient client = Resilient.of( PaymentClient.class, new HttpPaymentClient(), registry); client.charge(order); // retried, guarded, timed out
PaymentClient$Resilient.java// Generated by the resilience4cata annotation processor. Do not edit. public final class PaymentClient$Resilient implements PaymentClient { private final PaymentClient delegate; private final RetryPolicy retry0; private final CircuitBreakerPolicy circuitBreaker0; private final TimeoutPolicy timeout0; // … constructor: each policy resolved once from the registry @Override public Receipt charge(Order p0) { final RetryPolicy r4c$retry = this.retry0; final long r4c$start = r4c$retry.start(); int r4c$attempts = 0; while (true) { try { return this.r4c$circuitBreaker0(p0); } catch (Throwable r4c$e) { final long r4c$delay = r4c$retry.onFailure(++r4c$attempts, r4c$e, r4c$start); if (r4c$delay < 0 || !r4c$retry.pause(r4c$delay)) { throw r4c$e; } } } } private Receipt r4c$circuitBreaker0(Order p0) { final CircuitBreakerPolicy r4c$cb = this.circuitBreaker0; final long r4c$token = r4c$cb.acquire(); if (r4c$token < 0) { throw r4c$cb.rejection(); } final long r4c$started = r4c$cb.startTimer(); try { final Receipt r4c$result = this.r4c$timeout0(p0); r4c$cb.onSuccess(r4c$token, r4c$started); return r4c$result; } catch (Throwable r4c$e) { r4c$cb.onFailure(r4c$token, r4c$started, r4c$e); throw r4c$e; } } private Receipt r4c$timeout0(Order p0) { final TimeoutPolicy r4c$timeout = this.timeout0; final TimeoutPolicy.Scope r4c$scope = r4c$timeout.enter(); final Receipt r4c$result; try { r4c$result = this.delegate.charge(p0); } catch (Throwable r4c$e) { r4c$timeout.failed(r4c$scope, r4c$e); throw r4c$e; } r4c$timeout.succeeded(r4c$scope); return r4c$result; } }
HttpPaymentClient.charge
Abridged: the real file uses fully qualified names.
The protection is code you can read
Resilience4j and MicroProfile wrap your calls in decorators or proxies at runtime. resilience4cata does that work in the compiler, so what runs is ordinary Java.
Step into it with a debugger
The processor writes PaymentClient$Resilient next to your own classes. Every policy is a few straight lines in a method you can open, set a breakpoint in and read in a stack trace.
at HttpPaymentClient.charge at PaymentClient$Resilient.r4c$timeout0 at PaymentClient$Resilient.r4c$circuitBreaker0 at PaymentClient$Resilient.charge at CheckoutService.pay
Nothing allocated when calls succeed
Synchronous calls that succeed or are rejected allocate zero bytes. An allocation gate runs the benchmarks on every pull request and fails the build if that changes.
Mistakes fail the build
A bad duration, a fallback with the wrong signature or an annotation on a final method is a compile error with a stable code and a fix, not a surprise in production.
error: [R4C-ATR-001] @Retry.delay = "2 seconds"
is not a valid duration: … Use a number
followed by ns, us, ms, s, m, h or d …
@Retry(delay = "2 seconds")
^
Time per call, logarithmic scale. Lower is better.
Show as a table
| Scenario | Threads | resilience4cata | Resilience4j |
|---|---|---|---|
| Retry, first attempt succeeds | 1 | 1.2 ns, 0 B | 22.9 ns, 0 B |
| Circuit breaker, every call succeeds | 4 | 1.9 ns, 0 B | 832 ns, 2 B |
| Sync timeout | 1 | 22.2 ns, 0 B | 8842 ns, 176 B |
| Bulkhead with free slots | 1 | 20.5 ns, 0 B | 20.4 ns, 0 B |
Eight policies, one annotation each
Put them on a method or on the whole interface. Stack them freely: the processor nests them in one well-chosen order, which @PolicyOrder can change.
@Retry
Try again with exponential backoff and jitter, within an optional time budget.
maxAttempts · delay · backoff · maxDuration@Hedge
Start the same idempotent async call again when it is slow, and keep the first success.
delay · maxAttempts · hedgeOn · budgetPercent@CircuitBreaker
Stop calling a dependency that keeps failing, then let a few probe calls through.
failureRateThreshold · waitInOpen · halfOpenCalls@RateLimiter
Admit a fixed number of calls per period, rejecting or waiting for the rest.
permits · period · maxWait@AdaptiveLimit
Cap concurrent calls with a limit that finds itself: up while latency holds, down when it rises.
algorithm · minLimit · maxLimit@Timeout
Interrupt a call that runs past its budget, on the caller’s own thread.
value@Bulkhead
Cap how many calls run at the same time, so one slow dependency can’t take every thread.
maxConcurrent · maxWait@Fallback
Return a useful answer when everything else has failed, from a sibling method or a handler class.
value · methodOrder, outermost first: Fallback, Retry or Hedge, CircuitBreaker, RateLimiter, AdaptiveLimit, Timeout, Bulkhead, then your method. How they combine
From dependency to first protected call
Add the processor
Core at runtime, annotations compile-only, the processor on the annotation processor path.
annotationProcessor 'dev.resilience4cata: resilience4cata-processor'
Annotate an interface
Durations are strings like
"200ms", checked when you compile.@Retry(maxAttempts = 3) @Timeout("2s") Receipt charge(Order order);
Wrap your implementation
One registry per application shares breaker and limiter state by name.
Resilient.of(PaymentClient.class, new HttpPaymentClient(), registry);
Annotation-driven resilience for plain Java 21+. Apache License 2.0.
dev.resilience4cata:resilience4cata-bom:0.10.0