[EN] Spring Boot 4 & Resilience4j: So elegant kann Fehlerbehandlung sein

Julian | Nov 22, 2025 min read

The year is 2025. Spring Boot 4 is here, and we no longer use deprecated RestTemplate calls. We combine the new Declarative HTTP Client feature with the proven Circuit Breaker Pattern and refine it with a Retry strategy.

In the previous article Curcuit Breaker Pattern with Spring Boot & Resilience4j we introduced the following code:

@Service
public class InventoryClient {
    // ... Alter RestTemplate Code ...
    // (Boilerplate Code mit manuellen Aufrufen)
}

Today we want to migrate this to Spring Boot 4 and at the same time make it more robust against short network wobbles.

Migration to Spring 4

1. Das Setup: Modern & Clean

In Spring Boot 4 we simply define external calls as an interface. We also add spring-retry so that every small “hiccup” doesn’t trigger the circuit breaker immediately.

The dependencies in the 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!)

Instead of a class with RestTemplate, we just write an interface. This is extremely elegant and readable.

// Im Order Service
// Wir definieren, wie der Inventory Service aussieht
@HttpExchange("/inventory")
public interface InventoryClient {

    @PostExchange("/reserve")
    void reserveItem(@RequestBody ReservationRequest request);
}

This requires a small config to activate the client (this is the part that tutorials often forget):

@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

Now comes the magic. A Circuit Breaker is great for total failures. But what if just a single network packet is lost? Do we then want to give up immediately? No.

We combine @Retryable (for transient errors) with @CircuitBreaker (for persistent failures).

The Logic:

  1. Retry: “Try it up to 3 times, maybe it was just a wobble.”
  2. Circuit Breaker: “If it still doesn’t work after 3 tries (or if errors keep coming), I’ll pull the ripcord.”
@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; 
    }
}

Note: @Recover is the fallback equivalent of @Retryable, but works similarly to Resilience4j’s fallbackMethod. If both annotations are used, the “outside” mechanism often takes effect.

4. Konfiguration in application.yml

Here we configure when Resilience4j throws out the backup. We solved the retry configuration directly via annotation above (but it can also be moved to the config).

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 

Conclusion

With Spring Boot 4 and the Declarative Clients, the code for microservice communication is drastically reduced.

However, the real strength lies in the combination:

  • Retry smooths out small bumps in the network.
  • Circuit Breaker protects the system from collapse in the event of real failures.

If you combine the two, you get a system that represents “state of the art” backend development in 2025: clean, resilient and stable.

More from me about Spring Boot 4