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?