Hook: Interviewers love this topic because one flaky downstream service can quietly take down an entire microservice if you do nothing.
Question: What is Resilience4j in Spring Boot, and why do microservices use it?
Answer: Resilience4j is a lightweight fault-tolerance library for Java and Spring Boot. It helps a service survive failures in other services by wrapping risky calls with patterns like CircuitBreaker, Retry, Bulkhead, and RateLimiter. Instead of letting every request hang or fail slowly, you can fail fast, return a fallback, and protect user experience.
Interview-Ready Answer: "In Spring Boot, I use Resilience4j to protect microservices from unstable dependencies. It gives me patterns like circuit breaker, retry, bulkhead, and rate limiting. My go-to pattern is the circuit breaker: if a downstream payment or inventory service keeps failing, the breaker opens, requests fail fast, and I return a fallback instead of exhausting threads and timeouts. That keeps latency predictable and prevents one bad dependency from taking down the whole app."
Resilience means your service keeps working, or at least fails gracefully, when a dependency is slow or broken. Resilience4j is not a server and not a proxy; it is a small Java library that sits inside your Spring Boot app and wraps the code that talks to other systems. In Spring Boot, you usually use annotations or the programmatic API, and Spring creates a proxy (a wrapped object that intercepts method calls) around your bean.
CallNotPermittedException instead of waiting for a timeout.The mental model is simple: a circuit breaker is like an electric fuse. When a dependency is repeatedly failing, it trips so the rest of the system does not burn out.
| Pattern | Solves | Use when | Watch out |
|---|---|---|---|
| Retry | Transient glitches | Timeouts, 503s | Do not retry non-idempotent writes |
| Circuit Breaker | Repeated failure | Bad downstream dependency | Needs enough calls to trip |
| Bulkhead | Resource isolation | One dependency may hog threads | Does not fix bad logic |
| Rate Limiter | Traffic bursts | Protect APIs or quotas | Not the same as auth |
In real Spring Boot projects, you often combine them: retry for short network hiccups, circuit breaker for a truly sick dependency, and bulkhead to stop one slow downstream call from consuming all request threads.
resilience4j-spring-boot3 starter; Boot 2 used different starter artifacts, but the ideas are the same.Latency overhead is usually tiny compared with a network hop. The real value is not speed; it is preventing thread pileups, timeout storms, and cascading failures.
Throwable parameter. Keep the return type the same.Bottom line: Resilience4j gives you small, focused tools to keep one broken dependency from becoming a full outage.
Imagine a checkout service in an e-commerce app. It calls a payment provider and then an inventory service before confirming the order. One afternoon the payment provider starts timing out for 8-10 seconds per request. Without Resilience4j, Tomcat request threads pile up, the app becomes slow for everyone, and a few failed payment calls can cascade into a full outage.
With Resilience4j, the checkout service trips a circuit breaker after repeated failures. New requests fail fast, the app returns a friendly payment pending response, and the order can be queued for a later retry or manual recovery. On the logs, you might see messages like CallNotPermittedException: CircuitBreaker 'paymentGateway' is OPEN. On the dashboard, latency drops because the app stops waiting on a dead dependency.
What goes wrong when people misunderstand it: a team adds retries around a payment call with no idempotency key. During a partial outage, the same card charge is attempted multiple times. Users report duplicate charges, support tickets spike, and logs show repeated retry attempts plus downstream timeout errors. The fix is not just "add retry"; it is "add retry only for safe operations, and protect risky ones with a circuit breaker and idempotency controls."
package com.example.resilience4jdemo;
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
@SpringBootApplication
public class Resilience4jDemoApplication {
public static void main(String[] args) {
SpringApplication.run(Resilience4jDemoApplication.class, args);
}
// Small thresholds make the circuit breaker visibly change state in a short demo.
// In production you tune these based on real traffic and latency data.
@Bean
public CircuitBreaker paymentCircuitBreaker() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // open when at least half of recent calls fail
.slidingWindowSize(4) // small demo window; production often uses a larger value
.minimumNumberOfCalls(4)
.permittedNumberOfCallsInHalfOpenState(2)
.waitDurationInOpenState(Duration.ofSeconds(5))
.build();
return CircuitBreaker.of("paymentGateway", config);
}
@Bean
public Retry paymentRetry() {
RetryConfig config = RetryConfig.custom()
.maxAttempts(3) // one initial try + two retries
.waitDuration(Duration.ofMillis(200))
.ignoreExceptions(CallNotPermittedException.class) // once the breaker is open, do not keep retrying it
.build();
return Retry.of("paymentGatewayRetry", config);
}
@Bean
public PaymentGatewayClient paymentGatewayClient() {
return new PaymentGatewayClient();
}
@Bean
public CheckoutService checkoutService(PaymentGatewayClient client,
CircuitBreaker paymentCircuitBreaker,
Retry paymentRetry) {
return new CheckoutService(client, paymentCircuitBreaker, paymentRetry);
}
@Bean
public CheckoutController checkoutController(CheckoutService checkoutService) {
return new CheckoutController(checkoutService);
}
@Bean
CommandLineRunner demo(CheckoutService checkoutService) {
return args -> {
System.out.println("---- Resilience4j demo starting ----");
for (int i = 1; i <= 8; i++) {
String result = checkoutService.placeOrder(new BigDecimal("19.99"));
System.out.println("Call " + i + " -> " + result);
}
// Edge case: validation errors should not be hidden behind a fallback.
try {
checkoutService.placeOrder(new BigDecimal("0"));
} catch (Exception ex) {
System.out.println("Validation case -> " + ex.getClass().getSimpleName() + ": " + ex.getMessage());
}
};
}
@RestController
@RequestMapping("/api")
public static class CheckoutController {
private final CheckoutService checkoutService;
public CheckoutController(CheckoutService checkoutService) {
this.checkoutService = checkoutService;
}
@GetMapping("/checkout")
public String checkout(@RequestParam(defaultValue = "19.99") BigDecimal amount) {
return checkoutService.placeOrder(amount);
}
}
public static class CheckoutService {
private final PaymentGatewayClient paymentGatewayClient;
private final CircuitBreaker circuitBreaker;
private final Retry retry;
public CheckoutService(PaymentGatewayClient paymentGatewayClient,
CircuitBreaker circuitBreaker,
Retry retry) {
this.paymentGatewayClient = paymentGatewayClient;
this.circuitBreaker = circuitBreaker;
this.retry = retry;
}
public String placeOrder(BigDecimal amount) {
// Bad input is a caller problem, not a dependency problem.
if (amount == null || amount.signum() <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
Supplier<String> supplier = () -> {
String authCode = paymentGatewayClient.charge(amount);
return "payment approved, auth=" + authCode;
};
// Retry wraps the circuit breaker, so each failed attempt is recorded.
Supplier<String> protectedCall = Retry.decorateSupplier(
retry,
CircuitBreaker.decorateSupplier(circuitBreaker, supplier)
);
try {
return "ORDER CONFIRMED: " + protectedCall.get();
} catch (IllegalArgumentException ex) {
// Never hide validation mistakes.
throw ex;
} catch (Exception ex) {
return fallback(amount, ex);
}
}
private String fallback(BigDecimal amount, Throwable ex) {
// In a real service, you might create an order in PENDING state and publish an event.
return "ORDER PENDING: payment deferred for " + amount + " because "
+ ex.getClass().getSimpleName() + " - " + ex.getMessage();
}
}
public static class PaymentGatewayClient {
private final AtomicInteger attempts = new AtomicInteger();
public String charge(BigDecimal amount) {
int currentAttempt = attempts.incrementAndGet();
// Simulate a flaky provider: the first four attempts fail, later attempts succeed.
if (currentAttempt <= 4) {
throw new IllegalStateException("Payment provider timeout on attempt " + currentAttempt);
}
return "AUTH-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
}
}
}Follow-up & Tricky Questions:
resilience4j.* in application.yml or application.properties, or you create config beans in code when you want tighter control.@CircuitBreaker appear to do nothing? The most common reason is self-invocation: one method in the same class calls another annotated method directly, bypassing Spring’s proxy. Another common issue is missing the AOP starter when you rely on annotations.Common Mistakes:
Memory Hook: Picture Resilience4j as a building with fire doors: Retry is trying the elevator again, Circuit Breaker is locking the door when the hallway is on fire, and Bulkhead is closing the watertight compartment so one leak does not flood the ship.
Cheat Sheet:
resilience4j-spring-boot3 starter.Practice Tasks: