Skip to content
resilience4cata
ENES

Decisiones de diseño

Estado: la versión 0.10.0 está publicada en Maven Central. Las seis políticas del MVP funcionan de punta a punta, sincrónicas y async, en interfaces y en clases anotadas, junto con la configuración externa, los classifiers, la API programática, los eventos, los snapshots y el testkit; ver estado de la implementación. ¿Recién llegas? Empieza por el inicio rápido. Esta página también resume las decisiones de diseño tomadas antes del primer código (fase 1).

English version: en/design.md

resilience4cata es una librería de resiliencia para Java 21+ puro: circuit breaker, retry, timeout, bulkhead, rate limiter y fallback, declarados con anotaciones que funcionan sin Spring, CDI ni AspectJ, y pensada para tener menos overhead que Resilience4j.

Por qué otra librería de resiliencia

  1. Anotaciones sin framework. Las anotaciones de Resilience4j solo hacen algo a través de Spring AOP, AspectJ o Micronaut. El modo standalone de SmallRye Fault Tolerance es solo programático. Ninguna librería conocida ofrece resiliencia declarativa en Java puro (01 §1.3, §4.2).
  2. Rendimiento medible. Resilience4j asigna memoria en cada llamada (272 B/op para un circuit breaker solo, según su propio JMH), usa un lock en su ventana deslizante por defecto y compone las políticas con lambdas compartidas que el JIT no puede inlinear (01 §1.7, 03 §0). Polly v8 (.NET) demuestra que se puede llegar a casi cero asignaciones.
  3. Múltiples configuraciones como feature principal: configuraciones con nombre y herencia, perfiles, anotaciones compuestas propias como @ExternalCall, y overrides externos sin recompilar (04 §6).

Decisiones tomadas en la fase 1

Tema Decisión Detalle
Versión de Java Java 21+, probado en 21 y 25 arquitectura
Build Gradle con un DSL de Groovy mínimo; convention plugins escritos en Java; version catalog. Nada de Kotlin en el proyecto arquitectura
Mecanismo de anotaciones Procesador de anotaciones en compilación (APT) que genera código Java. Sin proxies, generación de bytecode ni agentes en core arquitectura
Frameworks Solo Java puro por ahora —
Módulos Nombres de una palabra: annotations, core, processor, config, testkit, micrometer, bom, más los internos benchmark, stress, examples arquitectura
Dependencias de core Ninguna salvo JSpecify (ver abajo) arquitectura
Políticas del MVP @CircuitBreaker, @Retry, @Timeout, @Bulkhead, @RateLimiter, @Fallback features
Contrato de rendimiento 0 B/op en el camino exitoso, sin locks en el camino caliente, verificado con JMH en CI features
Documentación Markdown en docs/en y docs/es, servido con Docsify. El inglés es la fuente de verdad arquitectura
Módulo annotations Separado de core arquitectura
Duraciones Strings ("250ms", "2s", "PT2S"), validados por el procesador features
Build de convenciones de Gradle Included build llamado conventions (respeta la regla de una palabra) arquitectura
Procesador Obligatorio. Sin fallback con proxies en runtime. Si falta la clase generada, Resilient.of falla al arrancar con un mensaje que explica cómo agregar el procesador. El código que no se puede anotar usa la API programática de core arquitectura
Retención de las anotaciones CLASS: las anotaciones no se cargan en runtime, así annotations puede ser una dependencia compileOnly. Pasar a RUNTIME más adelante es un cambio compatible si algún día se agrega un fallback con proxies arquitectura
Anotaciones en la interfaz vs la implementación Con Resilient.of(interfaz, impl, registry) solo cuentan las anotaciones de la interfaz. En el estilo subclase (Resilient.create(Impl.class, registry)), al que una clase entra declarando al menos una anotación propia, las anotaciones de la clase ganan, atributo por atributo: interfaz < método de la interfaz < clase < método de la clase (un método que sobrescribe uno de una superclase le gana). Los métodos que sobrescriben los de Object solo toman las anotaciones escritas en ellos arquitectura
Auto-invocación Se intercepta en el estilo subclase: this.otro() pasa por el método generado, así que se aplican sus políticas. No es posible en el estilo wrapper de interfaz, porque las llamadas dentro de la implementación del usuario nunca llegan al wrapper; queda documentado, y quien lo necesite anota una clase no final arquitectura
Anotaciones de nulabilidad JSpecify como dependencia real (api) de core: la única excepción a "cero dependencias". Es un jar de solo anotaciones de pocos KB, y da información de null al IDE, a Kotlin y a NullAway arquitectura
Recarga en caliente Ninguna. La configuración es inmutable una vez construido el registry; para cambiarla hay que reiniciar la aplicación. Así todos los valores de configuración quedan en campos final en el camino caliente features
Coordenadas Dominio resilience4cata.dev: groupId de Maven dev.resilience4cata, paquete raíz dev.resilience4cata arquitectura

Estado de la implementación

Pieza Estado
@Retry Terminado de punta a punta: anotación, validación y generación en el procesador, runtime RetryPolicy, registry con valores por defecto y configuraciones con nombre, anotaciones compuestas, combinación entre interfaz y método
@CircuitBreaker Terminado de punta a punta: ventanas por cantidad y por tiempo, máquina de estados sin locks, sondeos en half-open con maxWaitInHalfOpen, llamadas lentas, recordOn/ignoreOn, instancias compartidas por name, anidado dentro de @Retry, y forceOpen/disable/reset desde registry.circuitBreaker(id)
@Timeout Terminado de punta a punta (async: ver Métodos async). Métodos sincrónicos: la llamada sigue en el thread del llamador y un único watchdog compartido la interrumpe cuando se atrasa (resolución: timeout / 16, entre 1 y 50 ms, nunca antes de tiempo); la interrupción siempre se limpia después. Se aplica por intento, adentro del circuit breaker
@Bulkhead Terminado de punta a punta: límite tipo semáforo con maxConcurrent; con maxWait = "0" un contador sin locks cuyos rechazos solo leen, si no un Semaphore no justo que estaciona virtual threads sin fijarlos; instancias compartidas por name; capa más interna, así un slot se ocupa solo mientras corre el método y la espera de uno cuenta para @Timeout. Los métodos async con maxWait > 0 esperan sin bloquear: se encolan en orden FIFO, reciben directamente un slot liberado (la llamada arranca en un thread virtual nuevo) y se rechazan pasado maxWait; las llamadas sincrónicas y async comparten los slots, con una equidad aproximada entre ellas
@RateLimiter Terminado de punta a punta: permits por period con GCRA (generic cell rate algorithm) sobre un solo long: una ráfaga de hasta permits después de un rato sin llamadas, y luego un permiso cada period / permits; sin thread de fondo, y un rechazo solo lee. Con maxWait, la llamada reserva el próximo permiso y duerme hasta que toca; si la espera se interrumpe, el permiso se devuelve. Instancias compartidas por name, inspección con registry.rateLimiter(id). Capa dentro del circuit breaker, así las llamadas que rechaza el breaker no gastan permisos
@Fallback Terminado de punta a punta (async: ver Métodos async), como capa más externa, así también ve los rechazos (CallNotPermittedException, BulkheadFullException, RateLimitExceededException, CallTimeoutException). Dos formas, las dos verificadas al compilar: una clase handler tipada (FallbackHandler<T>, con T asignable al tipo de retorno, constructor público sin argumentos y una instancia por método) que recibe un FallbackContext con la falla, la cantidad de intentos y los argumentos; o métodos hermanos con los parámetros originales más un parámetro de excepción opcional, donde gana el tipo de excepción más específico. Con el wrapper de interfaz, los métodos hermanos tienen que ser métodos default, abstractos o static de la interfaz, porque la clase generada no puede llamar a los privados. applyOn/skipOn filtran las fallas (gana skipOn). Con @Retry, el fallback corre donde el bucle de retry se rinde, así conoce los intentos sin asignar nada en el camino exitoso. Una excepción checked que lanza un handler se relanza si el método la declara; si no, se envuelve en UndeclaredThrowableException. Los métodos genéricos están soportados: los métodos hermanos declaran los mismos parámetros de tipo (por posición, con los mismos límites) y se llaman con argumentos de tipo explícitos; una clase handler solo sirve cuando el tipo del resultado no depende de los parámetros de tipo del método
@Hedge Terminado (solo async) en 0.4.0: la misma llamada se vuelve a lanzar tras delay sin resultado, o enseguida tras una falla de hedgeOn; gana el primer éxito y los demás intentos se cancelan a través de cada política interna (un breaker no registra nada, un bulkhead libera el slot). Un int de progreso por llamada, cambiado con compare-and-set, decide el resultado exactamente una vez; las esperas van en el timer async sin locks (o exactas sobre un ManualClock); un presupuesto de hedge (activo por defecto) limita la carga extra. Excluyente con @Retry en un mismo método; los métodos sincrónicos son por ahora un error de compilación (funcionalidades)
@AdaptiveLimit Terminado en 0.6.0, sync y async: un bulkhead cuyo límite se mueve solo, entre minLimit y maxLimit, según la latencia de las llamadas (concurrency-limits de Netflix: GRADIENT2, el valor por defecto, o AIMD), recalculado una vez por ventana por el hilo que termina una llamada después de que la ventana terminó; los descartes (dropOn: timeouts y bulkheads llenos por defecto) hacen retroceder a AIMD; solo rechazo inmediato, todavía sin Vegas (página)
Métodos async Terminado para todas las políticas: un método que devuelve CompletionStage<T> o CompletableFuture<T> usa el camino async, decidido por el tipo de retorno (sin anotación extra). Una excepción lanzada antes de que el delegado devuelva un stage, o un stage null, cuenta como llamada fallida. Los reintentos se programan, nunca duermen: el primer intento corre en el thread del llamador, los siguientes en threads virtuales nuevos. El timeout falla el future con CallTimeoutException y cancela el stage atrasado (Future.cancel); corre en un timer sin locks (buzones repartidos por thread, un carril por duración, cancelación con un CAS de estado) que nunca dispara antes de tiempo y como mucho unos dos ticks tarde (tick = timeout async más corto / 16, entre 1 y 50 ms), y un stage que ya terminó no usa el timer. El bulkhead ocupa su slot hasta que el stage termina. El maxWait del rate limiter reserva el permiso y arranca la llamada cuando toca, sin bloquear. Los fallbacks pueden devolver T o un stage de T. Un solo thread de timer daemon para la librería; todo lo que dispara corre en un thread virtual. Los stages con comodín (CompletionStage<? extends X>, CompletableFuture<?>) están soportados: las políticas, los fallbacks y los classifiers trabajan con el límite superior X, y los fallbacks también pueden devolver un CompletionStage<? extends X>. Límites: sin ? super X ni tipos de stage propios; cancelar el future devuelto no cancela al delegado
classifier en @Retry / @CircuitBreaker Hecho, sincrónico y async: un Classifier<T> decide para resultados y excepciones (SUCCESS, FAILURE, IGNORE), verificado al compilar, una instancia por método. Reemplaza a retryOn/recordOn (combinarlos es un error de compilación); abortOn/ignoreOn siguen ganando. Un resultado FAILURE se reintenta y, cuando se agotan los intentos, se devuelve tal cual (sin @Fallback), o se lanza como ResultRejectedException, que pasa por @Fallback, con onExhaustedResult = THROW. Un classifier que lanza una excepción (el del retry o el del breaker) termina la llamada enseguida, con un evento GaveUp (CLASSIFIER_FAILED). Los métodos sin classifier generan el mismo código que antes (features)
API programática (Pipeline) Hecho: un pipeline inmutable construido a partir de un ResilienceRegistry para código que no se puede anotar; mismas políticas, orden y semántica que el código generado, sincrónico y async, con fallback y classifiers, run(...) para llamadas sin resultado, fallbacks async que devuelven un stage, y onExhaustedResult; sin asignar memoria en el camino exitoso sincrónico (features)
Eventos y snapshots Hecho: eventos tipados y sellados (transiciones y rechazos del breaker, reintentos, timeouts, rechazos del bulkhead y del rate limiter, fallbacks), registrados en el builder del registry, sin costo cuando nadie escucha; registry.snapshot() y snapshot() en los handles devuelven records de solo lectura (features)
Soporte de tests (testkit) Hecho: ManualClock controla reintentos, breakers, rate limiters, timeouts sincrónicos y async, esperas sincrónicas y async del bulkhead, y esperas async a través de Builder.ticker(...); jitter con semilla con Builder.random(...) (features)
Clases anotadas (estilo subclase) Terminado de punta a punta, sincrónico y async: una clase no final que declara una anotación de resiliencia obtiene una <Clase>$Resilient extends <Clase> generada, que se crea con Resilient.create(Tipo.class, registry, argumentosDelConstructor...) o con new Tipo$Resilient(registry, ...). Solo se sobrescriben los métodos con políticas, y cada uno termina con super.metodo(...), así que la auto-invocación (this.otro()) se intercepta. Las anotaciones de las interfaces implementadas y de las superclases se combinan atributo por atributo: interfaz < método de la interfaz < clase < método de la clase. Las clases final, sealed, abstractas, internas y privadas, los records, los enums y los métodos final anotados a nivel de método son errores de compilación. Una clase que solo implementa una interfaz anotada no se procesa: recibe las políticas de la interfaz con Resilient.of (features)
Configuración externa (módulo config) Hecho (MVP): archivos properties, recursos del classpath, variables de entorno (R4C_…) y propiedades del sistema (r4c.…); valores por defecto, configuraciones con nombre, extends, perfiles, overrides por instancia (instances.<id>.*, que ganan sobre los valores de las anotaciones); una configuración con nombre definida a la vez por código y externamente toma los valores externos atributo por atributo; validación estricta que informa todos los problemas a la vez con su fuente y un "¿quisiste decir…?" (features), y verifyInstances(registry) para ids de overrides que no corresponden a ninguna instancia.; locked = true en una anotación protege sus valores de los overrides por instancia (bloquear). Pendiente: YAML
Registro como procesador incremental de Gradle Hecho: el procesador está registrado como isolating (META-INF/gradle/incremental.annotation.processors); cada clase generada tiene como único elemento de origen la interfaz que envuelve, así que editar un archivo no relacionado recompila solo ese archivo, y editar un bundle, un handler o una superinterfaz reprocesa solo las interfaces que lo usan
Benchmarks JMH Retry (uno y cuatro servicios), circuit breaker (ventanas por cantidad y por tiempo), timeout, bulkhead y rate limiter (1 y 4 threads), métodos async, classifiers y el Pipeline programático, vs Resilience4j; corridas completas base contra nueva en paralelo en AWS con infra/bench.sh (benchmarks)
jcstress (módulo stress) Hecho: 21 tests jcstress de las políticas sin lock (contador del bulkhead, ráfaga y devolución del rate limiter, transiciones y estado quiet del circuit breaker, protocolo de sellos del timeout/watchdog, eventos de transición del breaker publicados una sola vez), verificados por mutación (17 de 18 mutantes detectados; un CAS solo release en el fin de llamada del timeout queda cubierto por un test, pero no se reprodujo en esta Mac). Se corren con ./gradlew :stress:jcstress -Pjcstress="-m quick"; ./gradlew build solo los compila
Códigos de error Hecho: cada diagnóstico del procesador (R4C-APT/ATR/CMP/ASY/CLS/FB/SUB), error de envoltura o de pipeline en runtime (R4C-RT), error del registry y de configuración de políticas (R4C-REG: valores inválidos, configuraciones con nombre desconocidas o duplicadas, instancias compartidas en conflicto, ids de instancia desconocidos, con "¿quisiste decir…?") y problema de configuración (R4C-CFG) tiene un código estable y una sección en la referencia de errores, con un ejemplo incorrecto y su corrección; el build compila los ejemplos del procesador
Publicación Preparada para Maven Central (r4c.publish, bom, centralBundle); 0.1.0 a 0.10.0 publicadas (release.yml) — ver Publicar una versión
Guía de usuario Hecha: primeros pasos, con cada snippet compilado y ejecutado por examples/.../GuideTest.java; README.md en la raíz para GitHub
CI (GitHub Actions) Hecho, en GitHub: build en JDK 21 y 25 en cada push y PR (./gradlew build -PtestJdk=N), alloc-gate en cada PR (./gradlew :benchmark:allocationCheck: 40 benchmarks contra sus presupuestos de B/op en benchmark/allocation-budgets.properties), stress (jcstress quick) después de cada push a main y cada noche, sonar (SonarCloud con cobertura), actionlint sobre los archivos de workflow; release (manual, sobre un tag v<versión>) corre primero el build completo; Dependabot para el catálogo de versiones y las actions, que están fijadas a SHAs de commit

Preguntas abiertas

Ninguna abierta. Las preguntas nuevas se agregan aquí.

Documentos de investigación

# Documento Tema
01 Panorama Resilience4j a fondo, competidores, oportunidades
02 Mecanismos de anotaciones APT vs proxies vs ByteBuddy vs agentes, con un JMH propio
03 Rendimiento interno Algoritmos y diseño sin locks por política, metodología de benchmarks
04 API y configuración Niveles de features, anotaciones, modelo de configuración
05 Arquitectura del proyecto Módulos, Gradle, JPMS, herramientas, docs, versionado

Sobre la evidencia. Cada documento de investigación marca sus afirmaciones: verificadas en código o documentación, medidas, o inferidas. El documento 04 se escribió de memoria, sin revisar las fuentes en internet. Hay que verificar sus referencias antes de apoyarse en ellas, sobre todo los nombres de anotaciones de SmallRye y el orden de ejecución de MicroProfile. Una afirmación de rendimiento sobre resilience4cata vale solo si está medida; las mediciones están en benchmarks.