lama-space logolama‑space
← Zurück zu Tech-Blog
Tech-Blog

Resilience4j in Spring Boot: Von null zu produktionsreifer Fehlertoleranz

Resilience4j in Spring Boot: Von null zu produktionsreifer Fehlertoleranz

Veröffentlicht auf lama-space.com, Medium und Java SPEKTRUM.

Warum dein Microservice ausfallen wird — und warum das in Ordnung ist

Jedes verteilte System fällt irgendwann aus. Ein nachgelagerter Service läuft in ein Timeout. Eine Datenbank ist kurzzeitig nicht erreichbar. Eine Drittanbieter-API drosselt dich bei Spitzenlast. In einem Monolithen sind solche Ausfälle eingedämmt. In einer Microservice-Architektur kann eine einzige langsame Abhängigkeit durch das gesamte System kaskadieren und Services lahmlegen, die mit dem ursprünglichen Problem nichts zu tun hatten.

Das nennt man einen kaskadierenden Ausfall — und er ist der größte Zuverlässigkeitskiller in großen Spring-Boot-Anwendungen.

Resilience4j ist die Antwort darauf. Es ist eine leichtgewichtige, modulare Fehlertoleranz-Bibliothek, speziell für Java 8+ und funktionale Programmierung gebaut. Es hat das inzwischen abgekündigte Netflix Hystrix als De-facto-Standard abgelöst und integriert sich nativ in Spring Boot, Micrometer-Metriken und Spring Cloud.

In diesem Artikel gehen wir von einem einfachen Circuit Breaker bis zu einem vollständigen Produktionsmuster — mit echtem Code, echter Konfiguration und echten Fehlern, die es zu vermeiden gilt.

Das Problem, das wir lösen

Stell dir folgende Architektur vor:

OrderService → PaymentService → BankingAPI (external, slow)
→ InventoryService → Database
→ NotificationService → EmailProvider

Wenn BankingAPI plötzlich 8 Sekunden statt 200ms braucht:

  • PaymentService-Threads stauen sich beim Warten
  • OrderService-Threads stauen sich beim Warten auf PaymentService
  • Der gesamte Bestellablauf verschlechtert sich
  • Nutzer sehen Timeouts auf völlig unabhängigen Seiten
  • Der Bereitschaftsdienst wird um 2 Uhr nachts gepiept

Resilience4j verhindert diese Kettenreaktion mit fünf Kernmodulen. Wir behandeln alle fünf.

Setup

Maven-Abhängigkeit

<!-- Core -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.0</version>
</dependency>
<!-- Required for annotations to work -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Metrics exposure (optional but recommended) -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

Hinweis: Für Spring Boot 3.x resilience4j-spring-boot3 verwenden. Für Spring Boot 2.x resilience4j-spring-boot2.

Modul 1: Circuit Breaker

Der Circuit Breaker ist das wichtigste Muster. Er überwacht Aufrufe an einen externen Service und "öffnet" den Kreis, sobald Fehler einen Schwellenwert überschreiten — weitere Aufrufe werden sofort abgewiesen, damit der nachgelagerte Service Zeit zur Erholung bekommt.

Die drei Zustände

CLOSED → Normalbetrieb, Aufrufe laufen durch
↓ (Fehlerrate überschreitet Schwellenwert)
OPEN → Aufrufe werden sofort abgelehnt, Fallback wird aufgerufen
↓ (nach waitDurationInOpenState)
HALF-OPEN → begrenzte Aufrufe werden zum Testen der Erholung durchgelassen
↓ (bei Erfolg)
CLOSED → zurück zum Normalbetrieb

Konfiguration (application.yml)

resilience4j:
circuitbreaker:
instances:
paymentService:
# How many calls to sample before calculating failure rate
slidingWindowSize: 10
# Open circuit when 50% of calls fail
failureRateThreshold: 50
# Also open if 60% of calls are too slow
slowCallRateThreshold: 60
# "Too slow" = longer than 2 seconds
slowCallDurationThreshold: 2s
# Stay open for 10 seconds before trying again
waitDurationInOpenState: 10s
# Allow 3 test calls in HALF-OPEN state
permittedNumberOfCallsInHalfOpenState: 3
# Minimum calls before circuit can open (avoids opening on 1/1 failures)
minimumNumberOfCalls: 5
# Which exceptions count as failures
recordExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
- feign.FeignException
# Which exceptions to ignore (e.g. business exceptions)
ignoreExceptions:
- com.example.exceptions.BusinessValidationException

Java-Implementierung

@Service
@RequiredArgsConstructor
@Slf4j
public class PaymentService {
private final BankingApiClient bankingApiClient;
private final PaymentRepository paymentRepository;
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
public PaymentResponse processPayment(PaymentRequest request) {
log.info("Processing payment for orderId: {}", request.getOrderId());
return bankingApiClient.charge(request);
}
// Fallback method — same signature + Throwable parameter
private PaymentResponse paymentFallback(PaymentRequest request, Throwable ex) {
log.warn("Payment circuit open or failed for orderId: {}. Cause: {}",
request.getOrderId(), ex.getMessage());
// Option 1: Queue for async retry
paymentRepository.saveForRetry(request);
return PaymentResponse.pending(request.getOrderId(),
"Payment queued. You will be notified when processed.");
// Option 2: Return cached/default response
// return PaymentResponse.degraded(request.getOrderId());
}
}

Wichtig: Die Fallback-Methode muss in der gleichen Klasse liegen, den gleichen Rückgabetyp, die gleichen Parameter plus ein Throwable am Ende haben. Stimmt die Signatur nicht, ignoriert Spring den Fallback stillschweigend.

Modul 2: Retry

Das Retry-Modul wiederholt einen fehlgeschlagenen Aufruf automatisch eine konfigurierbare Anzahl an Malen, optional mit exponentiellem Backoff. Einsetzen für transiente Fehler wie kurze Netzwerkaussetzer.

resilience4j:
retry:
instances:
inventoryService:
maxAttempts: 3
waitDuration: 500ms
# Exponential backoff: 500ms, 1000ms, 2000ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
# Only retry on these exceptions
retryExceptions:
- java.io.IOException
- org.springframework.web.client.ResourceAccessException
# Never retry on these (business errors)
ignoreExceptions:
- com.example.exceptions.InsufficientStockException
@Service
public class InventoryService {
private final InventoryClient inventoryClient;
@Retry(name = "inventoryService", fallbackMethod = "inventoryFallback")
@CircuitBreaker(name = "inventoryService", fallbackMethod = "inventoryFallback")
public StockResponse checkStock(String productId) {
return inventoryClient.getStock(productId);
}
private StockResponse inventoryFallback(String productId, Throwable ex) {
log.error("Inventory check failed after retries for product: {}", productId);
// Return cached stock data or conservative estimate
return StockResponse.unknown(productId);
}
}

Retry + CircuitBreaker kombinieren: @Retry immer innerhalb von @CircuitBreaker platzieren (Retry wird zuerst ausgewertet, dann CircuitBreaker). Die Reihenfolge der Annotationsverarbeitung ist wichtig — Retry läuft, bevor CircuitBreaker den Fehler zählt.

Modul 3: Rate Limiter

Schützt den eigenen Service davor, überlastet zu werden — entweder durch externe Clients, die ihn aufrufen, oder weil man selbst eine nachgelagerte API mit Rate Limits bombardiert.

resilience4j:
ratelimiter:
instances:
emailProvider:
# Allow max 10 calls per 1 second
limitForPeriod: 10
limitRefreshPeriod: 1s
# Wait up to 500ms for a permission before throwing
timeoutDuration: 500ms
@Service
public class NotificationService {
@RateLimiter(name = "emailProvider", fallbackMethod = "emailFallback")
public void sendEmail(EmailRequest request) {
emailProviderClient.send(request);
}
private void emailFallback(EmailRequest request, RequestNotPermitted ex) {
log.warn("Rate limit reached, queuing email for: {}", request.getRecipient());
emailQueue.add(request);
}
}

Modul 4: Bulkhead

Der Bulkhead isoliert Ressourcen, damit eine langsame Abhängigkeit nicht alle verfügbaren Threads verbraucht und den Rest der Anwendung aushungert. Benannt nach den wasserdichten Abteilen eines Schiffs — läuft eines voll, bleiben die anderen trocken.

Zwei Typen:

SemaphoreBulkhead (Standard) — begrenzt gleichzeitige Aufrufe über ein Semaphore:

resilience4j:
bulkhead:
instances:
reportingService:
# Max 5 concurrent calls to reporting (slow, resource-heavy)
maxConcurrentCalls: 5
# Wait up to 100ms for a slot before rejecting
maxWaitDuration: 100ms

ThreadPoolBulkhead — gibt der Abhängigkeit einen eigenen isolierten Thread-Pool (besser für blockierende Aufrufe):

resilience4j:
thread-pool-bulkhead:
instances:
reportingService:
maxThreadPoolSize: 5
coreThreadPoolSize: 3
queueCapacity: 10
@Service
public class ReportingService {
@Bulkhead(name = "reportingService", fallbackMethod = "reportFallback")
public Report generateReport(ReportRequest request) {
// Slow, resource-intensive operation
return reportGenerator.generate(request);
}
private Report reportFallback(ReportRequest request, BulkheadFullException ex) {
return Report.queued("Report is being generated. Check back in a few minutes.");
}
}

Modul 5: TimeLimiter

Erzwingt ein Timeout für asynchrone Operationen. Nützlich, wenn der zugrunde liegende Client kein eigenes Timeout konfiguriert hat (zum Beispiel RestTemplate ohne Timeout).

resilience4j:
timelimiter:
instances:
externalApiCall:
timeoutDuration: 3s
cancelRunningFuture: true
@Service
public class ExternalDataService {
private final ExternalApiClient externalApiClient;
@TimeLimiter(name = "externalApiCall", fallbackMethod = "dataFallback")
@CircuitBreaker(name = "externalApiCall")
public CompletableFuture<DataResponse> fetchExternalData(String query) {
return CompletableFuture.supplyAsync(() -> externalApiClient.fetch(query));
}
// TimeLimiter requires CompletableFuture — fallback must also return it
private CompletableFuture<DataResponse> dataFallback(String query, Throwable ex) {
return CompletableFuture.completedFuture(DataResponse.cached(query));
}
}

Muster kombinieren: Der vollständige Produktions-Stack

In einer echten Großanwendung kombiniert man alle Module. Die empfohlene Reihenfolge der Annotationen:

@TimeLimiter(name = "paymentService")
@Bulkhead(name = "paymentService")
@CircuitBreaker(name = "paymentService", fallbackMethod = "fallback")
@Retry(name = "paymentService")
@RateLimiter(name = "paymentService")
public CompletableFuture<PaymentResponse> processPayment(PaymentRequest request) {
...
}

Die Ausführungsreihenfolge (von innen nach außen): RateLimiter → Retry → CircuitBreaker → Bulkhead → TimeLimiter

Monitoring im Produktivbetrieb

Hier hören die meisten Tutorials auf — aber Produktivbetrieb ohne Observability ist Blindflug.

Metriken für Actuator freigeben

management:
endpoints:
web:
exposure:
include: health, metrics, prometheus
health:
circuitbreakers:
enabled: true
ratelimiters:
enabled: true

Health-Endpoint-Antwort

Öffnet sich ein Circuit, spiegelt Spring Boot Actuators /actuator/health das automatisch wider:

{
"status": "DOWN",
"components": {
"circuitBreakers": {
"status": "DOWN",
"details": {
"paymentService": {
"status": "DOWN",
"details": {
"state": "OPEN",
"failureRate": "60.0%",
"slowCallRate": "0.0%",
"bufferedCalls": 10,
"failedCalls": 6
}
}
}
}
}
}

Wichtige Prometheus/Grafana-Metriken im Blick behalten

# Circuit breaker state (0=CLOSED, 1=OPEN, 2=HALF_OPEN)
resilience4j_circuitbreaker_state{name="paymentService"}
# Failure rate percentage
resilience4j_circuitbreaker_failure_rate{name="paymentService"}
# Call outcomes
resilience4j_circuitbreaker_calls_total{name="paymentService", kind="successful"}
resilience4j_circuitbreaker_calls_total{name="paymentService", kind="failed"}
resilience4j_circuitbreaker_calls_total{name="paymentService", kind="not_permitted"}
# Retry attempts
resilience4j_retry_calls_total{name="inventoryService", kind="successful_with_retry"}
# Bulkhead available slots
resilience4j_bulkhead_available_concurrent_calls{name="reportingService"}

Ein Grafana-Dashboard mit diesen Metriken aufbauen. Einen Alert setzen, wenn failure_rate > 40% — man will es wissen, bevor der Circuit öffnet.

Häufige Fehler (und wie man sie vermeidet)

1. AOP-Abhängigkeit vergessen

Annotationen tun ohne spring-boot-starter-aop stillschweigend gar nichts.

2. Falsche Fallback-Signatur

Die Fallback-Methode muss exakt passen: gleiche Parameter + Throwable als letzter Parameter. Falsche Signatur = kein Fallback, nur eine Exception.

3. Annotierte Methoden intern aufrufen

// WRONG — AOP proxy is bypassed, Resilience4j is not applied
public void doSomething() {
this.processPayment(request); // same class, no proxy
}
// CORRECT — inject self or use programmatic API
@Autowired
private PaymentService self;
public void doSomething() {
self.processPayment(request);
}

4. slidingWindowSize zu klein gewählt

Ein slidingWindowSize: 5 mit minimumNumberOfCalls: 5 bedeutet, dass der Circuit schon nach 3 Fehlern in 5 Aufrufen öffnet. In Systemen mit hohem Traffic mag das beabsichtigt sein, aber bei Services mit wenig Traffic öffnet das den Circuit schon bei ganz normalem Rauschen. An das tatsächliche Traffic-Volumen anpassen.

5. Business-Exceptions nicht von technischen Exceptions unterscheiden

BusinessValidationException (falsche Eingabe vom Nutzer) ist kein Fehler des nachgelagerten Service. Immer zu ignoreExceptions hinzufügen — sonst öffnen Nutzerfehler den eigenen Circuit.

6. Retry ohne exponentiellen Backoff bei instabilen Services

Sofortige Wiederholungen bei einem angeschlagenen Service machen es schlimmer, nicht besser. In Produktion immer enableExponentialBackoff: true verwenden.

Praxisbeispiel: SAP-Integration

Wer über REST mit SAP integriert (ein gängiges Szenario in Logistik und Enterprise-Java), kennt das Problem: SAP-Systeme können während Wartungsfenstern langsam oder nicht erreichbar sein. Eine praxistaugliche Konfiguration:

resilience4j:
circuitbreaker:
instances:
sapIntegration:
slidingWindowSize: 20
failureRateThreshold: 40
slowCallRateThreshold: 50
slowCallDurationThreshold: 5s # SAP is allowed to be slow
waitDurationInOpenState: 30s # Give SAP time to recover
minimumNumberOfCalls: 10
recordExceptions:
- java.io.IOException
- org.springframework.web.client.HttpServerErrorException
- feign.RetryableException
ignoreExceptions:
- com.example.sap.SapValidationException
retry:
instances:
sapIntegration:
maxAttempts: 2 # Don't hammer SAP with retries
waitDuration: 2s
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
bulkhead:
instances:
sapIntegration:
maxConcurrentCalls: 10 # Limit concurrent SAP connections
maxWaitDuration: 200ms

Zusammenfassung

ModulSchützt vorEinsetzen wenn
Circuit BreakerKaskadierende AusfälleBei jedem externen Service-Aufruf
RetryTransiente NetzwerkfehlerNur bei idempotenten Operationen
Rate LimiterÜberlastung / API-LimitsExterne APIs mit Rate Limits
BulkheadThread-AushungerungLangsame oder ressourcenintensive Abhängigkeiten
TimeLimiterUnbegrenzte HängerAsync-Aufrufe ohne Timeout im Client

Resilience4j ist keine Wunderwaffe. Es verlangt, sorgfältig zu überlegen, was ein Fehler für jede einzelne Abhängigkeit bedeutet, was ein akzeptabler Fallback ist und wie man Schwellenwerte auf das tatsächliche Traffic-Volumen abstimmt. Die Muster sind einfach. Das Tuning ist die eigentliche Ingenieursarbeit.

Das vollständige Beispielprojekt zu diesem Artikel ist auf GitHub verfügbar: github.com/mohammedahmadi/resilience4j-spring-demo (folgt in Kürze).

„Resilience4j in Spring Boot: Von null zu produktionsreifer Fehlertoleranz“ teilen
Mohammed Ahmadi

Mohammed Ahmadi

Software Developer