Resilience Fundamentals

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is the concept chapter of the Resilience section: it explains what goes wrong when one service calls another, which pattern answers which problem, and how Resilience4j fits in. It was written against Resilience4j 2.4.0, Spring Boot 4.1.x, Spring Framework 7.0.x and Java 21, and the version claims were verified on 4 October 2026. Everything here is library-neutral except the code, which uses Resilience4j 2.4.0 and runs unchanged on Spring Boot 3.5.x (the Boot 3 differences are all in Getting Started).

The running example is Bookshelf, the same book-catalogue domain used by the FastAPI, NestJS, Django and Next.js sections: a bookshelf-service that calls a slow and flaky pricing-service, a third-party ISBN-metadata API with a request quota, an optional and expensive recommendations-service, and a reactive reviews-service.

Failure modes of remote calls

A call to another process is not a method call that happens to be slow. It crosses a network, a queue of threads and a dependency you do not control, and it can fail in ways a local call never does.

Failure mode What happens Why it hurts

Latency

The dependency answers, but late: a garbage-collection pause, a cold cache, a saturated database.

A slow answer is worse than an error. The caller’s thread, connection and memory stay occupied for the whole wait, and without a timeout the wait has no upper bound.

Partial failure

Some calls succeed and some fail: one replica is unhealthy, a deployment is half rolled out, a network path flaps.

The caller cannot tell "failed before it ran" from "ran, but the answer was lost", which is why retries need idempotency.

Cascading failure

A slow or failing dependency makes its callers slow, which makes their callers slow, until the whole call graph is down.

One degraded service takes out services that were perfectly healthy. The failure travels up the call chain.

Retry storm

Every layer retries a failing call, and the attempts multiply: three layers with three attempts each send 27 calls for one user request.

The retries meant to heal the problem keep the dependency overloaded, so it cannot recover.

Thundering herd

Many clients react to the same event at the same moment: a breaker closing, a cache entry expiring, a service restarting.

Synchronised retries or refills arrive as one spike. Randomised delays (jitter) spread them out.

Resource exhaustion

Threads, connections, sockets, memory or file descriptors are all consumed by calls that are waiting.

Capacity that other, unrelated calls need is gone. With virtual threads the threads are cheap, but the downstream connection pool, the database and the dependency itself are not.

The retry multiplication in code: three layers, three attempts each
AtomicInteger backendCalls = new AtomicInteger();
Supplier<String> backend = () -> {
    backendCalls.incrementAndGet();
    throw new IllegalStateException("503");
};

RetryConfig three = RetryConfig.custom()
    .maxAttempts(3)
    .waitDuration(Duration.ofMillis(1))
    .build();

Supplier<String> client  = Retry.decorateSupplier(Retry.of("client", three), backend);
Supplier<String> service = Retry.decorateSupplier(Retry.of("service", three), client);
Supplier<String> gateway = Retry.decorateSupplier(Retry.of("gateway", three), service);

assertThatThrownBy(gateway::get).isInstanceOf(IllegalStateException.class);
// backendCalls is 27: 3 x 3 x 3

Source: Resilience4j: Retry (the Retry.decorateSupplier and RetryConfig API). The amplification itself is a property of stacking retries, not of the library. The Google SRE book chapter on cascading failures describes the same effect at the scale of a whole fleet.

Latency is the failure mode that the rest of the section keeps returning to, because it is the one a plain try/catch cannot see. Spring Boot’s connect and read timeout properties have no default value, so a call written without them waits for as long as the underlying HTTP client allows, which is often far too long.

Timeouts are the first line of defence, and Boot has no default for them
spring:
  http:
    clients:
      connect-timeout: 1s
      read-timeout: 2s

Source: Spring Boot: Calling REST Services (the spring.http.clients.connect-timeout and spring.http.clients.read-timeout properties; the plural clients prefix is the Spring Boot 4 form, and the older singular spring.http.client.* keys no longer take effect). How these client timeouts nest with the Resilience4j TimeLimiter and the gateway timeout is covered in TimeLimiter.

The pattern catalogue

Each pattern removes one failure mode, and none removes all of them. The matrix below shows which pattern is the primary answer to which failure mode (solid dot) and which only helps, or helps only when configured carefully (hollow dot).

A matrix of six failure modes against eight resilience patterns. Solid dots mark the primary answer: a timeout or hedge for slow dependencies, retry or fallback for transient errors, circuit breaker and bulkhead for cascading failure, circuit breaker and rate limiter for retry storms, timeout and bulkhead for resource exhaustion and rate limiter for overload
Pattern The problem it solves Resilience4j module Spring Boot form

Timeout

A call that never returns holds its resources forever. Bound the wait.

resilience4j-timelimiter (TimeLimiter), plus the HTTP client’s own timeouts

@TimeLimiter, resilience4j.timelimiter.*

Retry

Transient failures (a dropped packet, a restarting pod) usually succeed on the next attempt.

resilience4j-retry (Retry)

@Retry, resilience4j.retry.*

Circuit breaker

A dependency that is failing or slow should be given time to recover instead of being hammered. Reject calls immediately ("fail fast") while it is unhealthy.

resilience4j-circuitbreaker (CircuitBreaker)

@CircuitBreaker, resilience4j.circuitbreaker.*

Bulkhead

Calls to one dependency must not consume all the threads or connections that other calls need. Cap the concurrency per dependency.

resilience4j-bulkhead (Bulkhead, ThreadPoolBulkhead)

@Bulkhead, resilience4j.bulkhead., resilience4j.thread-pool-bulkhead.

Rate limiter

A dependency has a quota, or you want to protect it from bursts. Cap the calls per period.

resilience4j-ratelimiter (RateLimiter)

@RateLimiter, resilience4j.ratelimiter.*

Fallback

When the call cannot succeed, return something useful (a cached value, a default, an empty list) instead of an error.

No dedicated module: Decorators.withFallback(…​) in resilience4j-all

fallbackMethod attribute on every annotation

Cache

Repeating an expensive call for the same input wastes the dependency’s capacity and adds latency.

resilience4j-cache (Cache, a JCache decorator)

No annotation and no Boot properties; decorated programmatically

Hedging

The slowest few per cent of calls dominate tail latency. Send a second, speculative request and take whichever answers first.

resilience4j-hedge (Hedge)

No Boot integration; decorated programmatically

The Bookshelf scenario uses each pattern where it fits best.

Downstream call Patterns that guard it

pricing-service: slow and flaky

CircuitBreaker + TimeLimiter, with a fallback to the last known price

Third-party ISBN-metadata API: a quota, and 429/503 answers

Client-side RateLimiter + Retry with exponential backoff and jitter, honouring Retry-After

recommendations-service: expensive and optional

ThreadPoolBulkhead, degrading to an empty list

reviews-service: called reactively through WebClient

The Reactor operators for the same patterns

The patterns compose, and the order matters: a retry placed outside a circuit breaker behaves differently from one placed inside it. That is the subject of Fallbacks & Composition; this page only needs the vocabulary.

The same patterns, wired by hand, look like this. Decorators is the builder from resilience4j-all, and the two exception types listed in withFallback are the ones that trigger the fallback:

A circuit breaker with a fallback that degrades the answer
public String priceLabel(Supplier<String> pricingCall) {
    return Decorators.ofSupplier(pricingCall)
        .withCircuitBreaker(breaker)
        .withFallback(
            List.of(CallNotPermittedException.class, RuntimeException.class),
            t -> "price unavailable")
        .decorate()
        .get();
}

Source: Resilience4j: CircuitBreaker examples and the v2.4.0 README (the Decorators builder).

Idempotency: the precondition for retries

A retry is only safe when repeating the call has the same effect as making it once. An operation that is idempotent can be repeated; one that is not can charge a customer twice.

  • GET, HEAD, PUT, DELETE and OPTIONS are defined as idempotent by RFC 9110. A GET for a book price is always safe to retry.

  • POST is not idempotent by definition. Retrying POST /orders after a read timeout may create two orders, because the first one may have succeeded.

  • A non-idempotent operation becomes retryable when the server deduplicates it. The usual contract is a client-generated key sent with every attempt of the same logical operation: the server stores the first result under that key and returns it for every repeat.

A retryable POST: one key per logical operation, reused by every attempt
public String placeOrder(String isbn) {
    String key = UUID.randomUUID().toString();   // generated once, outside the retry
    Supplier<String> call = () -> http.post().uri("/orders")
        .header("Idempotency-Key", key)
        .body(new Order(isbn, 1))
        .retrieve().body(String.class);
    return Retry.decorateSupplier(retry, call).get();
}

Source: Resilience4j: Retry and RFC 9110: HTTP Semantics (method idempotency). The server side of the Idempotency-Key contract is shown for NestJS in Resilience, Idempotency & Locks; the Spring Boot retry rules (which methods and exceptions to retry) are in Retry.

Messaging is the exception that proves the rule. Consumer-side retries on Kafka are native to the platform (DefaultErrorHandler, @RetryableTopic), with their own back-off, dead-letter topics and offset semantics, so Resilience4j is not used for them. See Kafka messaging with Spring Boot and Spring Boot and Kafka.

Fail fast versus degrade gracefully

When a dependency is unhealthy, the service can answer in one of two ways, and the choice is a product decision as much as a technical one.

Fail fast returns an error immediately. It is right when a wrong or empty answer would be worse than none (a payment, a stock reservation) and when the caller can retry or surface the error. A circuit breaker without a fallback fails fast: once it is open, CallNotPermittedException is thrown in microseconds, without touching the dependency.

Degrade gracefully returns a reduced answer. It is right when a stale or partial answer is still useful: the last known price, an empty recommendations list, a cached review count.

Both behaviours on the same breaker
// Fail fast: the exception reaches the caller
public String priceOrFail(Supplier<String> pricingCall) {
    return breaker.executeSupplier(pricingCall);
}

// Degrade: the fallback decides what the caller sees
public String priceLabel(Supplier<String> pricingCall) {
    return Decorators.ofSupplier(pricingCall)
        .withCircuitBreaker(breaker)
        .withFallback(
            List.of(CallNotPermittedException.class, RuntimeException.class),
            t -> "price unavailable")
        .decorate()
        .get();
}

Run against a dependency that is down, the first call to priceLabel returns price unavailable, the breaker opens after its second recorded failure, and priceOrFail then throws CallNotPermittedException in roughly 0.2 ms.

Two rules keep degradation honest. A fallback must be cheaper and more reliable than the call it replaces: a fallback that calls another remote service just moves the problem. And a degraded answer should be recognisable as such (the stale flag on the Bookshelf Money record), so the caller and the metrics can tell the difference.

Where resilience belongs

Resilience can live in four places, and they are complementary, not alternatives.

Layer What it is good at What it cannot do

Client library (Resilience4j inside the service)

Application-aware decisions: which exceptions count as failures, what the fallback returns, per-method configuration, business-level metrics.

It protects only the process it runs in, and every language in the fleet needs its own library.

Service mesh (sidecar proxies such as Istio)

Language-neutral timeouts, retries, outlier ejection and connection limits, applied uniformly without code changes.

It sees HTTP/gRPC requests, not your domain: it cannot return a "last known price", and its retry rules match on status codes and connection errors, not on whether the operation is idempotent.

API gateway (API gateway and BFF)

Protecting the edge: rate limits per client, a circuit breaker per route, a default fallback response.

It does not protect calls between internal services.

Platform (Kubernetes probes, autoscaling, resource limits)

Replacing dead instances and adding capacity (health probes and lifecycle).

It reacts in seconds to minutes, far slower than a single call needs to decide.

A mesh and Resilience4j can both retry, and then the multiplication from the retry-storm example returns. Pick one layer to own retries for a given call, and let the others only enforce timeouts and limits.

Inside a Spring Boot service, resilience belongs at the boundary component that owns the remote call (the client, adapter or repository facade), not in controllers or business logic. That keeps one instance per remote backend and a single place to find the fallback. The Bookshelf pricing client is the model used throughout the section:

Resilience on the client component, not in the controller
@Component
public class PricingClient {
    private final RestClient http;

    public PricingClient(RestClient.Builder builder,
                         @Value("${bookshelf.pricing.url}") String baseUrl) {
        this.http = builder.baseUrl(baseUrl).build();
    }

    @CircuitBreaker(name = "pricing", fallbackMethod = "lastKnownPrice")
    public Money price(String isbn) {
        return http.get().uri("/prices/{isbn}", isbn).retrieve().body(Money.class);
    }

    Money lastKnownPrice(String isbn, Throwable t) {
        return new Money(isbn, 0, true);
    }
}

Source: Resilience4j: Spring Boot 2 & 3 (annotation usage). This is the class built and run in Getting Started.

A short history: Hystrix to Resilience4j

Hystrix, written at Netflix, popularised the circuit breaker and bulkhead for the JVM. Its README now states that it is no longer in active development and is in maintenance mode, with 1.5.18 as the last release. Netflix says it keeps using Hystrix for existing applications and leverages projects such as Resilience4j for new ones. Resilience4j was designed for functional programming rather than as a port of Hystrix, and its readme.io page Comparison to Netflix Hystrix lists the differences:

  • Decorators instead of commands. Hystrix wraps a call in a HystrixCommand subclass. Resilience4j provides higher-order functions that enhance any functional interface, lambda or method reference.

  • Stackable and optional. You pick only the decorators you need, and you can combine several on one function.

  • Synchronous or asynchronous. Any decorated function can be run synchronously or with CompletableFuture, plus Reactor and RxJava operators for reactive types.

  • Slow-call awareness. The circuit breaker can open when too many calls exceed a duration threshold, even before exceptions appear. Hystrix decides from errors and timeouts.

  • A configurable half-open probe. Hystrix runs one trial call in the half-open state; Resilience4j runs a configurable number and compares the failure rate to a threshold.

  • Both emit event streams for operators to monitor.

The same pricing call, in both libraries:

Hystrix (maintenance mode): a command class
public class PriceCommand extends HystrixCommand<Long> {
    private final String isbn;

    public PriceCommand(String isbn) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("pricing"))
            .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                .withExecutionTimeoutInMilliseconds(1000)
                .withCircuitBreakerErrorThresholdPercentage(50)));
        this.isbn = isbn;
    }

    @Override protected Long run() { return pricing.priceCents(isbn); }
    @Override protected Long getFallback() { return 0L; }
}

long cents = new PriceCommand("978-0").execute();
Resilience4j: a decorated lambda
Supplier<Long> price = () -> pricing.priceCents(isbn);

long cents = Decorators.ofSupplier(price)
    .withCircuitBreaker(breaker)
    .withFallback(t -> 0L)
    .decorate()
    .get();

Source: Netflix Hystrix README (the maintenance-mode statement, version 1.5.18) and Resilience4j: Comparison to Netflix Hystrix. A property and concept mapping table for migrating existing Hystrix code is in Migration & What’s Changed.

For a Spring application today the supported route is Resilience4j directly, or through the provider-neutral Spring Cloud CircuitBreaker abstraction.

Resilience4j’s design

  • Functional decorators. The unit of composition is a function. CircuitBreaker.decorateSupplier(cb, call) returns another Supplier with the same signature, so a decorated call can be passed anywhere the original was used.

  • Modular. Each pattern is its own artifact (resilience4j-circuitbreaker, -retry, -ratelimiter, -bulkhead, -timelimiter, -cache, -hedge), so an application adds only what it uses.

  • No dependencies beyond its own modules. The core modules depend on resilience4j-core and a small set of internal modules. The README lists the core modules, and the integrations (Spring, Reactor, RxJava, Kotlin, Feign, Micrometer) are separate add-ons, so the core never pulls in a framework.

  • Registry, configuration and instance. Every module follows the same model: a registry holds named configurations and creates named instances. Spring Boot fills the registries from application.yml. See Registries, Configuration & Decorators.

  • Events and metrics everywhere. Each instance publishes events and exposes metrics that Micrometer can bind.

  • Java 17 baseline. The 2.4.0 artifacts target Java 17 bytecode, so they run on the Java 21 used throughout the section.

Source: Resilience4j README at v2.4.0 and the v2.4.0 release; the Java 17 baseline and the module list come from the 2.4.0 build files, and the 2.0.x changes from the repository’s RELEASENOTES.adoc.

The same idea in other stacks

The vocabulary on this page is not specific to Spring. Other stacks on this site ship the same patterns as annotations, middleware or policy objects.

Stack Library Where it is covered

Quarkus

SmallRye Fault Tolerance (MicroProfile Fault Tolerance): @Timeout, @Retry, @CircuitBreaker, @Bulkhead, @Fallback, @RateLimit

Fault Tolerance and Resilience. For a side-by-side with Spring Boot see Quarkus vs Spring Boot.

NestJS

@nestjs/resilience: timeout, retry, circuit breaker, bulkhead and fallback decorators (pre-1.0 at the time of writing)

Resilience, Idempotency & Locks

.NET

Polly and Microsoft.Extensions.Http.Resilience: resilience pipelines with retry, circuit breaker, timeout, hedging and rate limiter strategies

HTTP client and resilience in ASP.NET Core

The mapping is close: a Resilience4j CircuitBreaker is a SmallRye @CircuitBreaker is a Polly circuit-breaker strategy. The differences are in the defaults and the details (sliding-window types, slow-call rate, how failures are classified), which is why each Resilience4j page lists every property with its default.

Distributed rate limiting with a shared store is not something Resilience4j does. Bucket4j, the usual library for it on the JVM, is covered in this same section: Resilience4j versus Bucket4j compares the two (including the same idea in other stacks) and Bucket4j: Distributed Rate Limiting covers the shared store.

Cascading failure, with and without a circuit breaker

The two diagrams below follow one slow dependency through the Bookshelf call chain. Without a breaker every layer waits, its threads fill up, and the caller’s callers start failing too.

flowchart LR U[Users] --> G[Gateway] G --> B[bookshelf-service] B -->|calls wait for the timeout| P[pricing-service
slow, all calls hang] B -.->|threads and connections fill up| B G -.->|bookshelf-service stops answering,
gateway threads fill up| G classDef bad fill:#fbe3e3,stroke:#b03030,color:#222 class P,B,G bad
Figure 1. Without a circuit breaker: the failure travels up the call chain

With a breaker on the pricing call, the first few slow or failed calls are recorded, the circuit opens, and the following calls are rejected immediately and answered from the fallback. The threads are free again, so the rest of the service keeps working while pricing-service recovers.

flowchart LR U[Users] --> G[Gateway] G --> B[bookshelf-service] B --> CB{CircuitBreaker
pricing} CB -->|OPEN: rejected in microseconds| F[Fallback:
last known price] CB -.->|CLOSED or HALF_OPEN:
trial calls only| P[pricing-service
recovering] F --> B classDef ok fill:#e3f1e3,stroke:#2f7a2f,color:#222 classDef bad fill:#fbe3e3,stroke:#b03030,color:#222 class B,G,F ok class P bad
Figure 2. With a circuit breaker: the failure is contained at the boundary

The breaker’s states, its thresholds and its tuning are the subject of CircuitBreaker.

What changed recently

  • Hystrix has been in maintenance mode since 2018 and is not an option for new work. Resilience4j is the maintained JVM library recommended in its place.

  • Resilience4j 2.0.0 removed Vavr as a dependency and moved to Java 17; 2.0.1 added resilience4j-spring-boot3 and resilience4j-spring6 for Spring Boot 3. 2.4.0 (March 2026) is the current release, and resilience4j-spring-boot4 is the starter for Spring Boot 4. The full list is in Migration & What’s Changed.

  • Spring Framework 7 added a small built-in retry and concurrency-limit facility (@Retryable, @ConcurrencyLimit). It covers part of the catalogue, and the section compares it with Resilience4j in Spring Framework Native Resilience.

Where to go next