Wir schreiben das Jahr 2025. Spring Boot 4 ist da, und wir nutzen keine veralteten RestTemplate-Aufrufe mehr. Wir kombinieren das neue Declarative HTTP Client Feature mit dem bewährten Circuit Breaker Pattern und verfeinern es mit einer Retry-Strategie.
Im vorherigen Artikel Curcuit Breaker Pattern mit Spring Boot & Resilience4j haben wir folgenden Code eingeführt:
@Service
public class InventoryClient {
// ... Alter RestTemplate Code ...
// (Boilerplate Code mit manuellen Aufrufen)
}
Diesen wollen wir heute auf Spring Boot 4 migrieren und gleichzeitig robuster gegen kurze Netzwerkwackler machen.
Migration zu Spring 4
1. Das Setup: Modern & Clean
In Spring Boot 4 definieren wir externe Aufrufe einfach als Interface. Zudem fügen wir spring-retry hinzu, um nicht bei jedem kleinen “Schluckauf” sofort den Circuit Breaker auszulösen.
Die Dependencies in der pom.xml (Spring Boot 4 Starter):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. Der Declarative Client (Neu in Spring Boot 4!)
Statt einer Klasse mit RestTemplate, schreiben wir nur noch ein Interface. Das ist extrem elegant und lesbar.
// Im Order Service
// Wir definieren, wie der Inventory Service aussieht
@HttpExchange("/inventory")
public interface InventoryClient {
@PostExchange("/reserve")
void reserveItem(@RequestBody ReservationRequest request);
}
Dazu braucht es eine kleine Config, um den Client zu aktivieren (das ist oft der Teil, den Tutorials vergessen):
@Configuration
public class ClientConfig {
@Bean
public InventoryClient inventoryClient(WebClient.Builder builder) {
WebClient webClient = builder.baseUrl("http://inventory-service").build();
return HttpServiceProxyFactory
.builder(WebClientAdapter.forClient(webClient))
.build()
.createClient(InventoryClient.class);
}
}
3. The Perfect Match: Circuit Breaker + Retry
Jetzt kommt die Magie. Ein Circuit Breaker ist super bei totalen Ausfällen. Aber was ist, wenn nur ein einziges Netzwerkpaket verloren geht? Wollen wir dann sofort aufgeben? Nein.
Wir kombinieren @Retryable (für transiente Fehler) mit @CircuitBreaker (für persistente Ausfälle).
Die Logik:
- Retry: “Probier es bis zu 3-mal, vielleicht war es nur ein Wackler.”
- Circuit Breaker: “Wenn es nach 3 Versuchen immer noch nicht geht (oder dauernd Fehler kommen), ziehe ich die Reißleine.”
@Service
public class InventoryService {
private final InventoryClient inventoryClient;
public InventoryService(InventoryClient inventoryClient) {
this.inventoryClient = inventoryClient;
}
// REIHENFOLGE IST WICHTIG:
// 1. Circuit Breaker überwacht die Gesundheit des Services
@CircuitBreaker(name = "inventory", fallbackMethod = "reserveInventoryFallback")
// 2. Retry fängt kurze Exceptions ab, bevor sie als 'Fehler' zählen (optional)
// oder versucht es einfach erneut, bevor wir aufgeben.
@Retryable(retryFor = { Exception.class }, maxAttempts = 3, backoff = @Backoff(delay = 500))
public boolean reserveInventory(String orderId, String sku) {
// Der eigentliche Aufruf über das Interface
inventoryClient.reserveItem(new ReservationRequest(orderId, sku));
return true;
}
// FALLBACK: Wird gerufen, wenn alle Retries erschöpft sind ODER der Circuit OPEN ist
// Wichtig: Die Exception im Parameter sagt uns, warum wir hier sind.
@Recover
public boolean reserveInventoryFallback(Throwable t, String orderId, String sku) {
// Loggen, aber nicht abstürzen!
System.out.println("⚠️ Inventory Service nicht erreichbar: " + t.getMessage());
System.out.println("Status: Retries erschöpft oder Circuit Breaker OPEN.");
// Wir geben 'false' zurück, damit der Orchestrator weiß: "Hat nicht geklappt, aber ich lebe noch."
return false;
}
}
Hinweis: @Recover ist das Fallback-Pendant zu @Retryable, funktioniert aber ähnlich wie die fallbackMethod von Resilience4j. Wenn beide Annotationen genutzt werden, greift oft der Mechanismus, der “außen” liegt.
4. Konfiguration in application.yml
Hier konfigurieren wir, wann Resilience4j die Sicherung rauswirft. Die Retry-Konfiguration haben wir oben direkt per Annotation gelöst (kann aber auch in die Config ausgelagert werden).
resilience4j:
circuitbreaker:
instances:
inventory:
registerHealthIndicator: true
slidingWindowSize: 5 # Bei den letzten 5 Requests schauen
failureRateThreshold: 50 # Wenn 50% fehlschlagen...
waitDurationInOpenState: 10s # ...dann mach für 10s dicht (OPEN)
permittedNumberOfCallsInHalfOpenState: 3
Fazit
Mit Spring Boot 4 und den Declarative Clients wird der Code für Microservice-Kommunikation drastisch reduziert.
Die wahre Stärke liegt jedoch in der Kombination:
- Retry glättet kleine Unebenheiten im Netzwerk.
- Circuit Breaker schützt das System vor dem Kollaps bei echten Ausfällen.
Kombiniert man beides, erhält man ein System, das “State of the Art” Backend-Entwicklung im Jahr 2025 repräsentiert: Sauber, resilient und stabil.
