When the circuit breaker is open, your system stops forwarding requests to a failing service and instead returns a default response.

Here’s how to configure it:

Default Responses with Hystrix

Hystrix, a popular resiliency library, allows you to define fallback logic when a command fails or times out. This fallback can include returning a default response.

Let’s say you have a UserCommand that fetches user data from a remote service. If this command fails, you want to return a default User object.

import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandProperties;

public class UserCommand extends HystrixCommand<User> {

    private final String userId;

    public UserCommand(String userId) {
        super(HystrixCommand.Setter
            .withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserGroup"))
            .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                .withExecutionTimeoutInMilliseconds(500) // Set a timeout
                .withCircuitBreakerEnabled(true)
                .withCircuitBreakerRequestVolumeThreshold(10) // Open after 10 failures
                .withCircuitBreakerSleepWindowInMilliseconds(5000) // Stay open for 5 seconds
            ));
        this.userId = userId;
    }

    @Override
    protected User run() throws Exception {
        // Simulate a call to a remote service that might fail
        System.out.println("Calling remote user service for ID: " + userId);
        if (Math.random() < 0.7) { // 70% chance of failure
            throw new RuntimeException("Remote service unavailable");
        }
        return new User(userId, "User " + userId);
    }

    @Override
    protected User getFallback() {
        System.out.println("Returning fallback response for ID: " + userId);
        // Return a default user object
        return new User(userId, "Default User");
    }

    public static void main(String[] args) throws InterruptedException {
        // Simulate multiple requests to trigger the circuit breaker
        for (int i = 0; i < 20; i++) {
            User user = new UserCommand("user_" + i).execute();
            System.out.println("Received: " + user.getName());
            Thread.sleep(100); // Small delay between requests
        }
    }

    // Simple User class for demonstration
    static class User {
        String id;
        String name;

        User(String id, String name) {
            this.id = id;
            this.name = name;
        }

        public String getName() {
            return name;
        }

        @Override
        public String toString() {
            return "User{" +
                   "id='" + id + '\'' +
                   ", name='" + name + '\'' +
                   '}';
        }
    }
}

In this example:

  • run(): This method contains the actual logic to call the remote service. We simulate failure with Math.random().
  • getFallback(): This method is executed if run() throws an exception or times out. Here, we create and return a User object with a default name.
  • HystrixCommandProperties.Setter(): We configure the circuit breaker to open after 10 consecutive failures (withCircuitBreakerRequestVolumeThreshold(10)) and stay open for 5 seconds (withCircuitBreakerSleepWindowInMilliseconds(5000)). We also set a timeout of 500ms for the run() method.

When you run the main method, you’ll observe that initially, the remote service is called. After several failures, the circuit breaker will open, and getFallback() will be invoked, returning "Default User" for subsequent requests until the sleep window elapses.

Resilience4j

Resilience4j is a more modern alternative to Hystrix. Its fallback mechanism is similar.

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.circuitbreaker.CircuitBreaker.State;
import io.vavr.control.Try;

import java.time.Duration;
import java.util.function.Supplier;

public class Resilience4jFallbackExample {

    // Simulate a remote service call
    public static String callRemoteService(String input) {
        System.out.println("Calling remote service with: " + input);
        if (Math.random() < 0.7) { // 70% chance of failure
            throw new RuntimeException("Service is down!");
        }
        return "Success for " + input;
    }

    // Default response supplier
    public static String getDefaultResponse(Throwable throwable) {
        System.out.println("Returning fallback response due to: " + throwable.getMessage());
        return "Default Response";
    }

    public static void main(String[] args) {
        CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
            .failureRateThreshold(50) // Open circuit if failure rate exceeds 50%
            .waitDurationInOpenState(Duration.ofSeconds(5)) // Stay open for 5 seconds
            .permittedNumberOfCallsInHalfOpenState(3) // Allow 3 calls in half-open state
            .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
            .slidingWindowSize(10) // Consider last 10 calls for failure rate
            .build();

        CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.of(circuitBreakerConfig);
        CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("myService");

        // Decorate the remote service call with the circuit breaker
        Supplier<String> decoratedServiceCall = CircuitBreaker.decorateSupplier(circuitBreaker,
            () -> callRemoteService("some_input"));

        System.out.println("Circuit breaker state: " + circuitBreaker.getState());

        // Simulate requests
        for (int i = 0; i < 15; i++) {
            Try<String> result = Try.ofSupplier(decoratedServiceCall)
                                   .recover(Resilience4jFallbackExample::getDefaultResponse); // Apply fallback
            System.out.println("Request " + (i + 1) + ": " + result.get());

            // Simulate a pause to observe state changes
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        System.out.println("Final circuit breaker state: " + circuitBreaker.getState());
    }
}

In this Resilience4j example:

  • callRemoteService(): This simulates your actual service call.
  • getDefaultResponse(): This is your fallback function, accepting a Throwable to provide context.
  • CircuitBreakerConfig: We define the circuit breaker’s behavior: a 50% failure rate threshold, a 5-second wait in the open state, and a sliding window of 10 calls.
  • CircuitBreaker.decorateSupplier(): This wraps your service call supplier with circuit breaker logic.
  • Try.ofSupplier(...).recover(): This is the core of the fallback. Try handles potential exceptions from the supplier, and recover applies the getDefaultResponse function if an exception occurs.

Initially, you’ll see calls to the remote service. As failures accumulate, the circuit breaker will open, and the getDefaultResponse method will be invoked, printing "Default Response".

The key advantage of using circuit breakers with default responses is that it prevents cascading failures. When a downstream service is struggling, your application can gracefully degrade its functionality by returning cached data, a static default, or a simplified response, rather than overwhelming the failing service further or returning errors to the end-user. This improves user experience and system stability.

The next logical step is to explore how to implement adaptive default responses based on the context of the failing request.

Want structured learning?

Take the full Circuit-breaker course →