Installation
Two lines are enough to start: resilience4cata-all and the annotation processor. This page covers that setup, the individual modules, Gradle and Maven, IDEs, JPMS and other annotation processors.
Requirements: Java 21 or newer, both to compile and to run (the jars are built with --release 21 and tested on Java 21 and 25). Coordinates: groupId dev.resilience4cata, version 0.10.0, on Maven Central.
The artifacts
| Artifact | What it is | Scope |
|---|---|---|
resilience4cata-bom |
Aligns the versions of every artifact below | platform(...) (Gradle), import (Maven) |
resilience4cata-all |
From 0.8.0. One dependency for core, annotations and config, with no code of its own (the short way) |
implementation (Gradle), default compile (Maven) |
resilience4cata-core |
Policies, ResilienceRegistry, Resilient, Pipeline, events, snapshots. Its only dependency is JSpecify (org.jspecify:jspecify) |
implementation (Gradle), default compile (Maven) |
resilience4cata-annotations |
@Retry, @CircuitBreaker, @RateLimiter, @Timeout, @Bulkhead, @Fallback, @ResiliencePolicy. CLASS retention, so not needed at runtime |
compileOnly (Gradle), provided (Maven) |
resilience4cata-processor |
The annotation processor that generates the code. Mandatory when you use the annotations: there is no reflection fallback | annotationProcessor (Gradle), annotationProcessorPaths (Maven). Never on the runtime class path |
resilience4cata-config |
Optional. External configuration: properties files, environment variables, system properties (configuration guide) | implementation / default scope |
resilience4cata-micrometer |
Optional, from 0.3.0. Micrometer metrics: counters from events, gauges for breakers, bulkheads and rate limiters (Micrometer guide). Brings in micrometer-core |
implementation / default scope |
resilience4cata-spring |
Optional, from 0.4.0. Spring Boot 4.1+ auto-configuration: the registry from application.yml, annotated beans protected, health indicator, actuator endpoint, metrics (Spring Boot guide). Brings core and config |
implementation / default scope |
resilience4cata-micronaut |
Optional, from 0.5.0. Micronaut 5.2+ integration (needs Java 25, the one module above Java 21): the registry from application.yml, @ResilientBean interface beans and annotated classes protected, health indicator, management endpoint, metrics (Micronaut guide). Brings core and config |
implementation / default scope |
resilience4cata-quarkus |
Optional, from 0.5.0. Quarkus 3.40+ extension: the registry from application.properties, annotated beans and REST clients protected, readiness check, metrics, build-time checks (Quarkus guide). Brings core and config; Quarkus adds resilience4cata-quarkus-deployment at build time |
implementation / default scope |
resilience4cata-testkit |
Optional. ManualClock and ResilienceTesting for deterministic tests (testing guide). Does not depend on JUnit |
testImplementation / test |
resilience4cata-junit |
Optional, since 0.3.0. JUnit Jupiter extension (@ResilienceTest): a fresh ManualClock and a registry on it for each test, as parameters (testing guide). Brings testkit; needs JUnit 5.10 or newer, JUnit 6 included |
testImplementation / test |
core and processor must have the same version; the BOM makes sure they do.
The short way
resilience4cata-all depends on core, annotations and config, so one dependency gives you the policies, the annotations and external configuration. The processor still needs its own line: build tools only run annotation processors from the processor path, so no dependency can bring it.
Gradle
dependencies {
implementation 'dev.resilience4cata:resilience4cata-all:0.10.0'
annotationProcessor 'dev.resilience4cata:resilience4cata-processor:0.10.0'
}
Gradle (Kotlin)
dependencies {
implementation("dev.resilience4cata:resilience4cata-all:0.10.0")
annotationProcessor("dev.resilience4cata:resilience4cata-processor:0.10.0")
}
Maven
<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>
Write the same version on both lines: core and processor must match. The annotations end up on the runtime class path too; they are a small jar that nothing reads at run time. To keep them compile-only, or to leave out config, add the modules one by one as below.
Module by module
Import the BOM first, then add the artifacts without versions.
Gradle
dependencies {
implementation platform('dev.resilience4cata:resilience4cata-bom:0.10.0')
annotationProcessor platform('dev.resilience4cata:resilience4cata-bom:0.10.0') // processor path is resolved separately
implementation 'dev.resilience4cata:resilience4cata-core'
compileOnly 'dev.resilience4cata:resilience4cata-annotations'
annotationProcessor 'dev.resilience4cata:resilience4cata-processor'
implementation 'dev.resilience4cata:resilience4cata-config' // optional
testImplementation 'dev.resilience4cata:resilience4cata-testkit' // optional
}
Gradle (Kotlin)
dependencies {
val r4cBom = platform("dev.resilience4cata:resilience4cata-bom:0.10.0")
implementation(r4cBom)
annotationProcessor(r4cBom)
implementation("dev.resilience4cata:resilience4cata-core")
compileOnly("dev.resilience4cata:resilience4cata-annotations")
annotationProcessor("dev.resilience4cata:resilience4cata-processor")
implementation("dev.resilience4cata:resilience4cata-config") // optional
testImplementation("dev.resilience4cata:resilience4cata-testkit") // optional
}
Maven
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.resilience4cata</groupId>
<artifactId>resilience4cata-bom</artifactId>
<version>0.10.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.resilience4cata</groupId>
<artifactId>resilience4cata-core</artifactId>
</dependency>
<dependency>
<groupId>dev.resilience4cata</groupId>
<artifactId>resilience4cata-annotations</artifactId>
<scope>provided</scope>
</dependency>
<!-- optional -->
<dependency>
<groupId>dev.resilience4cata</groupId>
<artifactId>resilience4cata-config</artifactId>
</dependency>
<dependency>
<groupId>dev.resilience4cata</groupId>
<artifactId>resilience4cata-testkit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>21</release>
<annotationProcessorPaths>
<path>
<groupId>dev.resilience4cata</groupId>
<artifactId>resilience4cata-processor</artifactId>
<version>0.10.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Why each part is there:
- Gradle: the BOM on
annotationProcessortoo. Gradle resolves the processor path separately from the compile class path, so a platform added toimplementationdoes not give the processor its version. - Maven: the explicit
<version>in<path>.maven-compiler-pluginversions before 3.12.0 do not look at<dependencyManagement>for processor paths and fail with "version can neither be null, empty nor blank". From 3.12.0 on, the version can come from the imported BOM; writing it works with every plugin version. Keep it equal to the BOM's. - The processor goes on the processor path, not the class path. Since JDK 23,
javacno longer runs processors it finds on the class path unless asked to, and the processor has no business being in your runtime artifact.
The processor is registered with Gradle as an isolating incremental processor, so editing one file recompiles only what depends on it.
Annotated types in test sources
If you annotate interfaces or classes under src/test (test doubles, test-only clients), the test source set needs the annotations and the processor as well.
Gradle
dependencies {
testCompileOnly 'dev.resilience4cata:resilience4cata-annotations'
testAnnotationProcessor platform('dev.resilience4cata:resilience4cata-bom:0.10.0')
testAnnotationProcessor 'dev.resilience4cata:resilience4cata-processor'
}
Gradle (Kotlin)
dependencies {
testCompileOnly("dev.resilience4cata:resilience4cata-annotations")
testAnnotationProcessor(platform("dev.resilience4cata:resilience4cata-bom:0.10.0"))
testAnnotationProcessor("dev.resilience4cata:resilience4cata-processor")
}
Maven
Nothing to add: the maven-compiler-plugin configuration applies to testCompile too, and provided dependencies are on the test class path.
Spring Boot
For a Spring Boot 4.1+ application, depend on resilience4cata-spring instead of core (it brings core and config); the annotations and the processor stay as above. The Spring Boot guide covers the rest.
Gradle
dependencies {
implementation platform('dev.resilience4cata:resilience4cata-bom:0.10.0')
annotationProcessor platform('dev.resilience4cata:resilience4cata-bom:0.10.0')
implementation 'dev.resilience4cata:resilience4cata-spring' // brings core and config
compileOnly 'dev.resilience4cata:resilience4cata-annotations'
annotationProcessor 'dev.resilience4cata:resilience4cata-processor'
}
Gradle (Kotlin)
dependencies {
val r4cBom = platform("dev.resilience4cata:resilience4cata-bom:0.10.0")
implementation(r4cBom)
annotationProcessor(r4cBom)
implementation("dev.resilience4cata:resilience4cata-spring") // brings core and config
compileOnly("dev.resilience4cata:resilience4cata-annotations")
annotationProcessor("dev.resilience4cata:resilience4cata-processor")
}
Maven
<!-- with resilience4cata-bom imported in <dependencyManagement> -->
<dependency>
<groupId>dev.resilience4cata</groupId>
<artifactId>resilience4cata-spring</artifactId>
</dependency>
<dependency>
<groupId>dev.resilience4cata</groupId>
<artifactId>resilience4cata-annotations</artifactId>
<scope>provided</scope>
</dependency>
<!-- and resilience4cata-processor in annotationProcessorPaths, as above -->
Micronaut
For a Micronaut 5.2+ application (Java 25), depend on resilience4cata-micronaut instead of core (it brings
core and config); the annotations and the processor stay as above, next to Micronaut's own processor, in any
order. The compiler option -Ar4c.inject=micronaut lets Micronaut create the generated subclasses of annotated
classes. The Micronaut guide covers the rest.
Gradle
dependencies {
implementation platform('dev.resilience4cata:resilience4cata-bom:0.10.0')
annotationProcessor platform('dev.resilience4cata:resilience4cata-bom:0.10.0')
implementation 'dev.resilience4cata:resilience4cata-micronaut' // brings core and config
compileOnly 'dev.resilience4cata:resilience4cata-annotations'
annotationProcessor 'dev.resilience4cata:resilience4cata-processor'
}
tasks.withType(JavaCompile).configureEach {
options.compilerArgs.add('-Ar4c.inject=micronaut')
}
Gradle (Kotlin)
dependencies {
val r4cBom = platform("dev.resilience4cata:resilience4cata-bom:0.10.0")
implementation(r4cBom)
annotationProcessor(r4cBom)
implementation("dev.resilience4cata:resilience4cata-micronaut") // brings core and config
compileOnly("dev.resilience4cata:resilience4cata-annotations")
annotationProcessor("dev.resilience4cata:resilience4cata-processor")
}
tasks.withType<JavaCompile>().configureEach {
options.compilerArgs.add("-Ar4c.inject=micronaut")
}
Maven
<!-- with resilience4cata-bom imported in <dependencyManagement> -->
<dependency>
<groupId>dev.resilience4cata</groupId>
<artifactId>resilience4cata-micronaut</artifactId>
</dependency>
<dependency>
<groupId>dev.resilience4cata</groupId>
<artifactId>resilience4cata-annotations</artifactId>
<scope>provided</scope>
</dependency>
<!-- and resilience4cata-processor in annotationProcessorPaths, after micronaut-inject-java,
with <compilerArgs><arg>-Ar4c.inject=micronaut</arg></compilerArgs> -->
Quarkus
For a Quarkus 3.40+ application, depend on resilience4cata-quarkus instead of core (it brings core and config); the annotations and the processor stay as above. The Quarkus guide has the build snippets and covers the rest.
Verify that it works
Annotate one method of an interface (the quickstart has one) and compile. Next to the interface, the processor writes <Interface>$Resilient.java:
| Build | Main sources | Test sources |
|---|---|---|
| Gradle | build/generated/sources/annotationProcessor/java/main/<package>/ |
build/generated/sources/annotationProcessor/java/test/<package>/ |
| Maven | target/generated-sources/annotations/<package>/ |
target/generated-test-sources/test-annotations/<package>/ |
A nested interface gets its binary name: Repository.Cache becomes Repository$Cache$Resilient.
If the processor did not run, the first Resilient.of(...) fails fast, before any call, with:
[R4C-RT-003] No generated class com.acme.shop.clients.PriceClient$Resilient found. Make sure that com.acme.shop.clients.PriceClient has at least one resilience annotation, and that the annotation processor runs when it is compiled. With Gradle:
annotationProcessor 'dev.resilience4cata:resilience4cata-processor:<version>'
IDEs
The processor is plain javac annotation processing, so an IDE generates the classes as long as annotation processing is on for the project.
IntelliJ IDEA. Gradle projects build with Gradle by default (Settings → Build, Execution, Deployment → Build Tools → Gradle → Build and run using: Gradle), so the generated classes come from Gradle and nothing else is needed. If you switch that setting to IntelliJ IDEA, or for Maven projects, check Settings → Build, Execution, Deployment → Compiler → Annotation Processors → Enable annotation processing, reload the Maven or Gradle project so the processor path is imported, and rebuild.
Eclipse. For Maven projects, m2e configures annotation processing through m2e-apt: enable Preferences → Maven → Annotation Processing → Automatically configure JDT APT, or set the property <m2e.apt.activation>jdt_apt</m2e.apt.activation> in the POM. For Gradle projects (Buildship), check Project → Properties → Java Compiler → Annotation Processing and its Factory Path, which must list resilience4cata-processor (and its dependency resilience4cata-annotations).
VS Code. The Extension Pack for Java uses the Eclipse compiler underneath, so the Eclipse notes apply: Maven projects pick up annotationProcessorPaths, and for Gradle projects the setting java.import.gradle.annotationProcessing.enabled must stay on (its default).
If an IDE shows Resilient.of failing with R4C-RT-003 while the command-line build works, the IDE did not run the processor: rebuild the project after enabling it.
JPMS
In a named module, read core (and config or testkit if you use them), read the annotations only at compile time, and export each package that holds annotated types to core, which instantiates the generated classes reflectively:
module com.acme.shop {
requires dev.resilience4cata.core;
requires static dev.resilience4cata.annotations;
requires dev.resilience4cata.config; // optional
exports com.acme.shop.clients to dev.resilience4cata.core;
}
With resilience4cata-all, requires dev.resilience4cata.all; replaces the three requires: it reads core, the annotations and config transitively.
Without the qualified export, Resilient.of fails with R4C-RT-005, whose message names the line to add. core requires JSpecify transitively, so you don't need to declare it.
With other annotation processors
resilience4cata works next to Lombok, MapStruct and other processors. Two rules:
- Every processor goes on the same processor path. With Maven, once
annotationProcessorPathsis set,javacruns only the processors listed there, so Lombok and MapStruct must be listed too. - Lombok goes first. If an annotated class uses Lombok to generate the constructor or the methods the subclass overrides, Lombok must run before resilience4cata, or the generated subclass does not see them and fails to compile (for example
constructor StockService in class StockService cannot be applied to given types). Annotated interfaces don't depend on the order.
Gradle
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.38'
annotationProcessor 'org.projectlombok:lombok:1.18.38' // first
implementation platform('dev.resilience4cata:resilience4cata-bom:0.10.0')
annotationProcessor platform('dev.resilience4cata:resilience4cata-bom:0.10.0')
implementation 'dev.resilience4cata:resilience4cata-core'
compileOnly 'dev.resilience4cata:resilience4cata-annotations'
annotationProcessor 'dev.resilience4cata:resilience4cata-processor'
}
Gradle (Kotlin)
dependencies {
compileOnly("org.projectlombok:lombok:1.18.38")
annotationProcessor("org.projectlombok:lombok:1.18.38") // first
val r4cBom = platform("dev.resilience4cata:resilience4cata-bom:0.10.0")
implementation(r4cBom)
annotationProcessor(r4cBom)
implementation("dev.resilience4cata:resilience4cata-core")
compileOnly("dev.resilience4cata:resilience4cata-annotations")
annotationProcessor("dev.resilience4cata:resilience4cata-processor")
}
Maven
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
</path>
<path>
<groupId>dev.resilience4cata</groupId>
<artifactId>resilience4cata-processor</artifactId>
<version>0.10.0</version>
</path>
</annotationProcessorPaths>
With MapStruct, add mapstruct-processor to the same path; if you also use Lombok, follow MapStruct's own instructions for it (the lombok-mapstruct-binding artifact).
-Xlint:all -Werror
Builds that turn every warning into an error work. javac warns ("No processor claimed any of these annotations") when an annotation present in the sources is claimed by no processor. When resilience4cata's processor is the only one on the path, it claims every annotation it sees; next to other processors, it claims only its own (dev.resilience4cata.annotation.*), so the others still see everything else. If the warning appears anyway, it lists annotations that another processor left unclaimed: add -Xlint:-processing to the compiler arguments.
Tip Declare each annotated interface in its own file. An annotated interface written as a second, non-public top-level type in another type's file makes the generated class trigger
javac'sauxiliaryclasslint warning, which-Werrorturns into an error.
Native image
GraalVM Native Image needs no configuration of your own; the jars bring it:
-
The processor writes reachability metadata for every generated class, in the class output next to it:
META-INF/native-image/dev.resilience4cata.generated/<generated class>/reachability-metadata.json. It registers the class's public constructors for reflection, which is whatResilient.ofandResilient.createuse to find them (once per instance, never per call). Native Image reads every file of that name underMETA-INF/native-imageon the class path. Forcom.acme.PaymentClient:{ "reflection": [ { "condition": {"typeReached": "com.acme.PaymentClient"}, "type": "com.acme.PaymentClient$Resilient", "allPublicConstructors": true } ] }For an interface, the entry applies once the interface is reached (an implementation of it has been initialized, which
Resilient.ofneeds anyway). For an annotated class, the condition isdev.resilience4cata.Resilient, becauseResilient.create(C.class, ...)can run beforeCis initialized; the array types of varargs constructors are registered too. -
coreinitializes its background threads at run time. The timeout watchdog, the async timeout timer and the async delay timer are started by lazy holder classes;coreshipsMETA-INF/native-image/dev.resilience4cata/resilience4cata-core/native-image.propertieswith--initialize-at-run-timefor those holders, so a framework that initializes classes at build time (Quarkus does by default) never puts a started thread in the image. Create theResilienceRegistryand the wrapped services at run time, not in a static initializer that runs during the image build. -
Calling the generated constructor directly (
new StockService$Resilient(registry, warehouse)) uses no reflection at all.
The metadata uses the single-file format of GraalVM for JDK 23 and later (see Reachability Metadata). With an older GraalVM, register each …$Resilient class with its public constructors in your own reflect-config.json. Native builds are not part of the test suite yet.
Troubleshooting
R4C-RT-003: no generated class. The processor did not run on the type, or the type has no resilience annotation.R4C-RT-004:coreandprocessorhave different versions. Use the BOM.R4C-RT-005: a named module does not export the package todev.resilience4cata.core.- Compile errors start with a code such as
[R4C-ATR-001]; each one has a section with a wrong example and the fix in the error reference. - More symptoms and fixes: troubleshooting.