← All articles

Spring Boot

Designing Resilient API Clients in Spring Boot

A practical framework for timeouts, retries, and meaningful failure handling.

4 min read
JavaSpring BootAPIsMicroservices

1. What does Resilience mean in software?

resilience is the ability of a system to continue providing an acceptable level of service when one or more of its dependencies fail or behave unexpectedly.

Modern applications rarely work in isolation. A typical microservice may depend on several other services:

Client
  |
  v
Order Service
  |
  +----> Payment Service
  |
  +----> Inventory Service
  |
  +----> Notification Service

If the Payment Service becomes slow or unavailable, the problem should not automatically bring down the Order Service.

Network calls fail in normal production conditions. A good API client assumes this from the start and makes the failure behavior explicit.

This is where resilience becomes important.

A resilient application anticipates failures such as:

  • A downstream service being unavailable
  • Network failures
  • Connection timeouts
  • Slow responses
  • Temporary errors
  • Sudden increases in traffic
  • Dependency returning HTTP 5xx errors
  • A downstream service recovering after an outage

Instead of allowing these failures to propagate through the system, we introduce mechanisms such as:

  • Timeouts — Don't wait indefinitely for a dependency.
  • Retries — Retry transient failures where appropriate.
  • Circuit breakers — Temporarily stop calling an unhealthy dependency.
  • Fallbacks — Provide an alternative response when a dependency is unavailable.
  • Rate limiting — Prevent excessive requests from overwhelming a service.

2. How does Spring Boot provide support for resilience?

Springboot provides supports for resilence using the Spring ecosystem together with Resilience4j. Its a lightweight fault-tolerance library designed for Java applications. It provides implementations of the major resilience patterns:

             Resilience4j
                  |
       +----------+----------+
       |          |          |
    Retry    Circuit     TimeLimiter
              Breaker
       |
    Bulkhead
       |
 Rate Limiter

In this below example, API client call can be protected by a circuit breaker.


@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
public PaymentResponse makePayment(PaymentRequest request) {
return paymentClient.makePayment(request);
}

Similarly, retry can be enabled:

@Retry(name = "paymentService")
public PaymentResponse makePayment(PaymentRequest request) {
    return paymentClient.makePayment(request);
}

These mechanisms can also be configured externally rather than hard-coded into the application.

For exmple:

resilience4j:
  retry:
    instances:
      paymentService:
        max-attempts: 3
        wait-duration: 500ms

3. How do we enable resilience in a Spring Boot project?

Simple way is to add Resilience4j’s SpringBoot dependency in the project.

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
</dependency>

Then configure the resilience configurations or policies in application.yml file

resilience4j:
  retry:
    instances:
      paymentService:
        max-attempts: 3
        wait-duration: 500ms

  circuitbreaker:
    instances:
      paymentService:
        sliding-window-size: 10
        failure-rate-threshold: 50
        wait-duration-in-open-state: 10s

Then the API client can be protected:

@Retry(name = "paymentService")
@CircuitBreaker(
    name = "paymentService",
    fallbackMethod = "paymentFallback"
)
public PaymentResponse makePayment(PaymentRequest request) {
    return paymentClient.makePayment(request);
}

fallback method

private PaymentResponse paymentFallback(
        PaymentRequest request,
        Throwable exception) {

    return PaymentResponse.failed(
        "Payment service is temporarily unavailable"
    );
}

The resulting flow becomes:

              API Client
                  |
                  v
             Retry Policy
                  |
                  v
          Circuit Breaker
                  |
                  v
          Payment Service
                  |
             +----+----+
             |         |
           Success    Failure
             |         |
             v         v
          Response   Fallback

But there is an important architectural consideration

Don’t blindly retry every failure.

If the payment request succeeds at the server but the response is lost because of a network failure, automatically retrying the request could potentially create a duplicate payment.

Therefore, resilience isn’t simply about adding @Retry and @CircuitBreaker.

Retry only safe operations

Retries can turn a temporary fault into success, but they can also create duplicate side effects. Restrict automatic retries to idempotent operations or requests with an idempotency key.

Ask these questions:

  • Is the operation idempotent?
  • Which failires are transient?
  • Which HTTP status codes should be retried ?
  • How many time should we try ?
  • Do we need expinential backoff ?
  • How long the cleint wait ?
  • What should happen when the dependency remains unavailable?
  • Should we provide a fallback?

Resilience is not about preventing failures. Failures are inevitable in distributed systems. Resilience is about ensuring that a failure in one component does not unnecessarily become a failure of the entire system.

Related articles