RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Spring Boot questions
MediumSpring Boot#607 min readJul 11, 2026

Resilience4j.

practice
learning
Practice modeTest yourself instead of reading straight through

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."

🧠 Memory Map
Memory map — visual summary of this topic

What Resilience4j is

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.

How the circuit breaker works under the hood

  1. A request calls a method that talks to a remote service, such as payments or inventory.
  2. Resilience4j records whether the call succeeded, failed, or was slow.
  3. While the breaker is Closed, calls flow normally. If failures cross the threshold, it switches to Open.
  4. In the Open state, calls are rejected immediately with CallNotPermittedException instead of waiting for a timeout.
  5. After the wait period, it moves to Half-Open, allows a few test calls, and closes again only if those calls succeed.

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.

When to use which pattern

PatternSolvesUse whenWatch out
RetryTransient glitchesTimeouts, 503sDo not retry non-idempotent writes
Circuit BreakerRepeated failureBad downstream dependencyNeeds enough calls to trip
BulkheadResource isolationOne dependency may hog threadsDoes not fix bad logic
Rate LimiterTraffic burstsProtect APIs or quotasNot 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.

Performance and configuration facts interviewers probe

  • Most resilience checks are O(1) per call; the library updates a few counters or rolling-window buckets.
  • Memory is tiny and usually tied to the sliding window size, often around 100 calls for the common circuit breaker defaults.
  • Common circuit breaker defaults are a count-based window of 100, failure rate threshold of 50%, minimum number of calls of 100, open-state wait of 60s, and 10 calls in half-open state.
  • In Spring Boot 3, use the 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.

Important edge cases

  • Self-invocation: if one method in the same class calls another annotated method directly, Spring proxy interception can be bypassed. Put the protected call behind another bean or call it through the proxy.
  • Fallback signature: the fallback method should match the original arguments and usually add a final Throwable parameter. Keep the return type the same.
  • Do not hide bad input: validation errors are not transient failures. If the user sends a negative amount, fail fast instead of routing it to a fallback.
  • Retry carefully: never retry money movement blindly unless you have idempotency keys or deduplication, or you may charge twice.

Bottom line: Resilience4j gives you small, focused tools to keep one broken dependency from becoming a full outage.

Real-world story: checkout service under pressure

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."

Spring Boot
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:

  • How is Resilience4j different from Hystrix? Hystrix is legacy and no longer the modern choice, while Resilience4j is lightweight, active, and fits Spring Boot and Micrometer well. It also uses small, focused modules instead of one giant framework.
  • What is the difference between Retry and Circuit Breaker? Retry keeps trying a call that may succeed soon, while circuit breaker stops calling a dependency that is already failing too much. Retry is for temporary glitches; circuit breaker is for protecting the system when failures are repeated.
  • What does Half-Open mean? Half-Open is the test phase after the breaker has been open for a while. Only a limited number of calls are allowed through, and if they succeed, the breaker closes again.
  • How do you configure it in Spring Boot? You usually set properties under resilience4j.* in application.yml or application.properties, or you create config beans in code when you want tighter control.
  • What is Bulkhead used for? Bulkhead isolates resources so one slow dependency does not consume all threads or permits. Think of it as a compartment inside a ship: one leak should not sink the whole vessel.
  • Does Resilience4j replace good error handling? No. It complements proper validation, timeouts, idempotency, and observability. If the request itself is bad, you should still fail fast with a clear error.
  • Is retry always safe for POST requests? No, not unless the operation is idempotent or protected by an idempotency key. Otherwise one client request can become multiple real side effects, like duplicate payments.
  • Why might @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.
  • Can a fallback hide real bugs? Yes. If you catch every exception and always return a fallback, you may hide programming errors and data problems. Good fallbacks are narrow and intentional.

Common Mistakes:

  • Retrying everything: candidates often retry payment or order creation blindly. Correction: retry only transient, safe, preferably idempotent calls.
  • Using fallback for bad input: they hide validation failures behind a graceful response. Correction: bad requests should fail fast with a clear error.
  • Ignoring call order: they do not think about whether Retry wraps CircuitBreaker or the other way around. Correction: the order changes what gets counted and what gets retried.
  • Forgetting proxy limits: they annotate a method and call it from the same class. Correction: make sure the call goes through Spring’s proxy, usually by calling another bean.

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 = in-process fault-tolerance library for microservices.
  • Use Retry for short-lived failures, Circuit Breaker for repeated failures.
  • Use Bulkhead to isolate resources and RateLimiter to cap bursts.
  • Common circuit breaker defaults: 100-call window, 50% failure threshold, 60s open wait, 10 half-open calls.
  • Never retry unsafe writes without idempotency.
  • Spring Boot 3 uses the resilience4j-spring-boot3 starter.

Practice Tasks:

  • Add a second endpoint that calls another flaky service and give it its own circuit breaker name.
  • Change the demo so the fallback returns a typed response object instead of a plain string.
  • Introduce a bulkhead or rate limiter and observe how the app behaves under concurrent requests.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

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(); } } }