feat: Implement Fallback design pattern #2846 (#3538)

* feat: implement Fallback design pattern with circuit breaker and service monitoring

* docs: add README for Fallback design pattern implementation

---------

Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com>
This commit is contained in:
Priyanshu
2026-08-16 21:31:20 +03:00
committed by GitHub
co-authored by Ilkka Seppälä
parent f1dcddfd3a
commit 7e0ddb01cd
11 changed files with 791 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
---
title: "Fallback Pattern in Java: Graceful Degradation in Microservices"
shortTitle: Fallback
description: "Learn about the Fallback pattern in Java design, which ensures microservice resilience and graceful system degradation when primary dependencies fail."
category: Resilience
language: en
tag:
- Cloud distributed
- Fault tolerance
- Microservices
---
## Intent of Fallback Design Pattern
The Fallback design pattern is a resiliency pattern used in microservices architecture to handle failures gracefully. It ensures that when a service is unavailable, fails, or times out, the system can continue to operate by providing an alternative response or executing a predefined fallback mechanism. This pattern enhances robustness and reliability by preventing cascading failures and improving the overall user experience.
## Detailed Explanation of Fallback Pattern with Real-World Examples
Real-world example
> Consider a movie streaming application like Netflix. The home page loads personalized recommendations for the logged-in user. If the recommendation microservice goes offline or is too slow, the user shouldn't see a broken page. Instead, the system falls back to a cached list of globally popular movies. While the response is degraded (not personalized), the application remains functional, providing a seamless user experience.
In plain words
> Fallback ensures that if a primary service call fails, the application falls back to a backup strategy (e.g. cached response, default value, or simplified service) rather than raising an error and failing completely.
Wikipedia says
> A fallback is a contingency option to be taken if the preferred choice is unavailable. In software, fallback mechanisms are crucial for fault tolerance, allowing systems to degrade gracefully rather than crash.
## Programmatic Example of Fallback Pattern in Java
This Java example demonstrates how the Fallback pattern can manage service failures, integrate with a Circuit Breaker, and apply timeout limits.
1. **Defining the Remote Service Interface**
The `RemoteService` interface represents any external dependency call.
```java
public interface RemoteService {
String execute() throws Exception;
}
```
2. **Defining the Primary Service and Fallback Service**
The `PrimaryService` simulates our main external dependency which may suffer from errors or latency. The `FallbackService` returns a cached or degraded static response.
```java
// Primary Service simulating latency and errors
var healthyPrimary = new PrimaryService("Healthy data from primary service", 10, false);
var failingPrimary = new PrimaryService("Failing service", 0, true);
var slowPrimary = new PrimaryService("Slow response from primary service", 500, false);
// Fallback Service providing degraded response
var fallback = new FallbackService("Fallback degraded/cached response");
```
3. **Monitoring Health with a Circuit Breaker**
A `SimpleCircuitBreaker` tracks the number of failures to trip the circuit to `OPEN`, bypassing the primary service immediately to avoid waiting for timeouts.
```java
// Trip after 2 failures; retry after 1 second
var circuitBreaker = new SimpleCircuitBreaker(2, 1000);
```
4. **Executing Calls with the FallbackExecutor**
The `FallbackExecutor` uses virtual threads to execute the primary service call. It applies timeouts, handles exceptions, records failures to the circuit breaker, and falls back to the fallback service as needed.
```java
try (var executor = new FallbackExecutor()) {
// Scenario 1: Healthy primary service call
String response1 = executor.execute(healthyPrimary, fallback, circuitBreaker, 100);
LOGGER.info("Response: {}", response1); // Healthy data from primary service
// Scenario 2: Failing service call triggers fallback
String response2 = executor.execute(failingPrimary, fallback, circuitBreaker, 100);
LOGGER.info("Response: {}", response2); // Fallback degraded/cached response
}
```
## When to Use the Fallback Pattern in Java
The Fallback pattern is applicable:
* In microservices architectures where dependencies are called over the network and are prone to network partitions, timeouts, and outages.
* When returning a default, empty, or cached value is preferable to failing the entire request.
* In user-facing systems where maintaining a working UI (even with degraded features) is critical for user satisfaction.
## Real-World Applications of Fallback Pattern in Java
* [Resilience4j Fallback mechanism](https://resilience4j.readme.io/docs/fallback)
* [Netflix Hystrix Fallback](https://github.com/Netflix/Hystrix/wiki/How-To-Use#Fallback)
* Spring Cloud Circuit Breaker integrations
## Benefits and Trade-offs of Fallback Pattern
Benefits:
* **Graceful Degradation**: Improves user experience by returning partial/cached data instead of errors.
* **Cascading Failure Prevention**: Avoids blocking threads waiting on hung services.
* **Fault Tolerance**: Improves system uptime and reliability.
Trade-Offs:
* **Stale Data**: Fallback cached responses may present out-of-date information to the user.
* **Increased Complexity**: Requires writing alternative execution flows and testing fallback scenarios.
## Related Patterns
- [Circuit Breaker](https://github.com/iluwatar/java-design-patterns/tree/master/circuit-breaker): Restricts calls to failing services. Often wraps the primary service before fallback is triggered.
- [Retry Pattern](https://github.com/iluwatar/java-design-patterns/tree/master/retry): Retries failed calls before triggering the fallback.
+70
View File
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
The MIT License
Copyright © 2014-2022 Ilkka Seppälä
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.iluwatar</groupId>
<artifactId>java-design-patterns</artifactId>
<version>1.26.0-SNAPSHOT</version>
</parent>
<artifactId>fallback</artifactId>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<configuration>
<archive>
<manifest>
<mainClass>com.iluwatar.fallback.App</mainClass>
</manifest>
</archive>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,99 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.fallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The Fallback design pattern is a resiliency pattern used in microservices architecture to handle
* failures gracefully. When a service is unavailable, fails, or times out, the system responds with
* a pre-configured fallback mechanism (like cached data or a simplified response).
*
* <p>This App demonstrates: 1. Healthy calls returning standard responses. 2. Failing calls
* (throwing exception) falling back to a fallback handler. 3. Latent calls (timing out) falling
* back. 4. Circuit Breaker tripping and immediately fast-failing to the fallback handler. 5.
* Recovery after a retry duration, returning the system to a healthy CLOSED state.
*/
public class App {
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
/**
* Main entry point for the application.
*
* @param args Command line arguments (not used)
*/
public static void main(String[] args) {
try (var executor = new FallbackExecutor()) {
var healthyPrimary = new PrimaryService("Healthy data from primary service", 10, false);
var failingPrimary = new PrimaryService("Failing service", 0, true);
var slowPrimary = new PrimaryService("Slow response from primary service", 500, false);
var fallback = new FallbackService("Fallback degraded/cached response");
// Failure threshold is 2 failures, retry time period is 1 second (1000ms)
var circuitBreaker = new SimpleCircuitBreaker(2, 1000);
// Scenario 1: Healthy primary service call
LOGGER.info("Scenario 1: Executing request to healthy primary service...");
String response1 = executor.execute(healthyPrimary, fallback, circuitBreaker, 100);
LOGGER.info("Response received: {}", response1);
LOGGER.info("Circuit Breaker State: {}\n", circuitBreaker.getState());
// Scenario 2: Failing primary service call (fails and increments failure count to 1)
LOGGER.info("Scenario 2: Executing request to failing primary service (throws exception)...");
String response2 = executor.execute(failingPrimary, fallback, circuitBreaker, 100);
LOGGER.info("Response received: {}", response2);
LOGGER.info("Circuit Breaker State: {}\n", circuitBreaker.getState());
// Scenario 3: Slow primary service call (times out and increments failure count to 2,
// tripping breaker)
LOGGER.info("Scenario 3: Executing request to slow primary service (triggers timeout)...");
String response3 = executor.execute(slowPrimary, fallback, circuitBreaker, 100);
LOGGER.info("Response received: {}", response3);
LOGGER.info("Circuit Breaker State: {}\n", circuitBreaker.getState());
// Scenario 4: Fast failing when circuit is OPEN
LOGGER.info("Scenario 4: Executing request while Circuit Breaker is OPEN...");
String response4 = executor.execute(healthyPrimary, fallback, circuitBreaker, 100);
LOGGER.info("Response received: {}", response4);
LOGGER.info("Circuit Breaker State: {}\n", circuitBreaker.getState());
// Scenario 5: Recovery from OPEN state
LOGGER.info("Scenario 5: Waiting for retry period to elapse...");
try {
Thread.sleep(1100); // Wait longer than retryTimePeriodMs (1000ms)
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
LOGGER.info(
"Circuit Breaker State (should be HALF_OPEN on next check): {}",
circuitBreaker.getState());
LOGGER.info("Executing request to healthy primary service to reset breaker...");
String response5 = executor.execute(healthyPrimary, fallback, circuitBreaker, 100);
LOGGER.info("Response received: {}", response5);
LOGGER.info("Circuit Breaker State (should be CLOSED): {}\n", circuitBreaker.getState());
}
}
}
@@ -0,0 +1,110 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.fallback;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Orchestrates primary service execution with timeouts, circuit breaker health checks, and fallback
* execution logic. Implements AutoCloseable to ensure thread pools are closed cleanly.
*/
public class FallbackExecutor implements AutoCloseable {
private static final Logger LOGGER = LoggerFactory.getLogger(FallbackExecutor.class);
private final ExecutorService executorService;
/** Constructor for FallbackExecutor. Initializes a virtual thread per task executor. */
public FallbackExecutor() {
this.executorService = Executors.newVirtualThreadPerTaskExecutor();
}
/**
* Executes the primary service call. If it fails, times out, or the circuit breaker is open, it
* falls back to the fallback service.
*
* @param primary the primary service call to execute
* @param fallback the fallback service to call when primary fails or is bypassed
* @param circuitBreaker the circuit breaker monitoring service health
* @param timeoutMs timeout limit for the primary service in milliseconds
* @return response string
*/
public String execute(
RemoteService primary,
RemoteService fallback,
SimpleCircuitBreaker circuitBreaker,
long timeoutMs) {
// 1. Check Circuit Breaker
if (circuitBreaker.getState() == SimpleCircuitBreaker.State.OPEN) {
LOGGER.warn("Circuit is OPEN. Fast-failing and calling fallback service.");
try {
return fallback.execute();
} catch (Exception ex) {
LOGGER.error("Fallback service execution failed: {}", ex.getMessage());
return "Fallback Error";
}
}
// 2. Attempt service call with timeout
Callable<String> task = primary::execute;
Future<String> future = executorService.submit(task);
try {
String result = future.get(timeoutMs, TimeUnit.MILLISECONDS);
circuitBreaker.recordSuccess();
return result;
} catch (TimeoutException e) {
LOGGER.error("Service call timed out. Triggering fallback.");
future.cancel(true); // Interrupt / cancel the task
circuitBreaker.recordFailure();
try {
return fallback.execute();
} catch (Exception ex) {
LOGGER.error("Fallback service execution failed: {}", ex.getMessage());
return "Fallback Error";
}
} catch (Exception e) {
LOGGER.error("Service call failed with exception: {}. Triggering fallback.", e.getMessage());
circuitBreaker.recordFailure();
try {
return fallback.execute();
} catch (Exception ex) {
LOGGER.error("Fallback service execution failed: {}", ex.getMessage());
return "Fallback Error";
}
}
}
@Override
public void close() {
executorService.shutdown();
}
}
@@ -0,0 +1,47 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.fallback;
/**
* A concrete implementation of the remote service representing the fallback handler. It is invoked
* when the primary service fails, returns a cached response or degrades gracefully.
*/
public class FallbackService implements RemoteService {
private final String fallbackResponse;
/**
* Constructor for FallbackService.
*
* @param fallbackResponse the fallback response to return
*/
public FallbackService(String fallbackResponse) {
this.fallbackResponse = fallbackResponse;
}
@Override
public String execute() {
return fallbackResponse;
}
}
@@ -0,0 +1,59 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.fallback;
/**
* A concrete implementation of the remote service representing the primary service. It can be
* configured to simulate latency and errors to test resilience features.
*/
public class PrimaryService implements RemoteService {
private final long latencyMs;
private final String response;
private final boolean shouldThrowException;
/**
* Constructor for PrimaryService.
*
* @param response the successful response to return
* @param latencyMs simulated latency in milliseconds
* @param shouldThrowException if true, the service will throw an exception
*/
public PrimaryService(String response, long latencyMs, boolean shouldThrowException) {
this.latencyMs = latencyMs;
this.response = response;
this.shouldThrowException = shouldThrowException;
}
@Override
public String execute() throws Exception {
if (shouldThrowException) {
throw new RuntimeException("Primary service failed!");
}
if (latencyMs > 0) {
Thread.sleep(latencyMs);
}
return response;
}
}
@@ -0,0 +1,36 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.fallback;
/** Representation of a service (e.g. a microservice client) that might fail. */
public interface RemoteService {
/**
* Executes the service logic.
*
* @return the service response
* @throws Exception if service call fails or is interrupted
*/
String execute() throws Exception;
}
@@ -0,0 +1,96 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.fallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** A simplified circuit breaker implementation for tracking remote service health. */
public class SimpleCircuitBreaker {
private static final Logger LOGGER = LoggerFactory.getLogger(SimpleCircuitBreaker.class);
private final int failureThreshold;
private final long retryTimePeriodMs;
private int failureCount = 0;
private long lastFailureTime = 0;
private State state = State.CLOSED;
/** The state of the circuit breaker. */
public enum State {
CLOSED,
OPEN,
HALF_OPEN
}
/**
* Constructor for SimpleCircuitBreaker.
*
* @param failureThreshold consecutive failure count threshold to trip the breaker
* @param retryTimePeriodMs time duration to wait in OPEN state before trying again (HALF_OPEN)
*/
public SimpleCircuitBreaker(int failureThreshold, long retryTimePeriodMs) {
this.failureThreshold = failureThreshold;
this.retryTimePeriodMs = retryTimePeriodMs;
}
/**
* Get the current state of the circuit breaker after evaluating transitions.
*
* @return current state
*/
public synchronized State getState() {
evaluateState();
return state;
}
private void evaluateState() {
if (state == State.OPEN) {
if (System.currentTimeMillis() - lastFailureTime > retryTimePeriodMs) {
state = State.HALF_OPEN;
LOGGER.info("Circuit Breaker transitioned to HALF_OPEN");
}
}
}
/** Records a successful operation, resetting the failure counter and closing the circuit. */
public synchronized void recordSuccess() {
failureCount = 0;
state = State.CLOSED;
LOGGER.info("Circuit Breaker transitioned to CLOSED (success recorded)");
}
/** Records a failure, potentially tripping the circuit to OPEN if threshold is met. */
public synchronized void recordFailure() {
failureCount++;
lastFailureTime = System.currentTimeMillis();
if (state == State.CLOSED && failureCount >= failureThreshold) {
state = State.OPEN;
LOGGER.warn("Circuit Breaker transitioned to OPEN (failure threshold reached)");
} else if (state == State.HALF_OPEN) {
state = State.OPEN;
LOGGER.warn("Circuit Breaker transitioned to OPEN (failed during HALF_OPEN)");
}
}
}
@@ -0,0 +1,37 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.fallback;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import org.junit.jupiter.api.Test;
/** Test verifying that App main method runs without throwing exceptions. */
class AppTest {
@Test
void testMain() {
assertDoesNotThrow(() -> App.main(new String[] {}));
}
}
@@ -0,0 +1,122 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.fallback;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
/** Unit and integration tests for the Fallback design pattern. */
class FallbackPatternTest {
@Test
void testHealthyServiceCall() {
try (var executor = new FallbackExecutor()) {
var primary = new PrimaryService("Primary Response", 0, false);
var fallback = new FallbackService("Fallback Response");
var circuitBreaker = new SimpleCircuitBreaker(2, 1000);
String response = executor.execute(primary, fallback, circuitBreaker, 100);
assertEquals("Primary Response", response);
assertEquals(SimpleCircuitBreaker.State.CLOSED, circuitBreaker.getState());
}
}
@Test
void testFailingServiceCall() {
try (var executor = new FallbackExecutor()) {
var primary = new PrimaryService("Primary Response", 0, true);
var fallback = new FallbackService("Fallback Response");
var circuitBreaker = new SimpleCircuitBreaker(2, 1000);
// First failure: should call fallback, state remains CLOSED since threshold is 2
String response1 = executor.execute(primary, fallback, circuitBreaker, 100);
assertEquals("Fallback Response", response1);
assertEquals(SimpleCircuitBreaker.State.CLOSED, circuitBreaker.getState());
// Second failure: should call fallback, state becomes OPEN
String response2 = executor.execute(primary, fallback, circuitBreaker, 100);
assertEquals("Fallback Response", response2);
assertEquals(SimpleCircuitBreaker.State.OPEN, circuitBreaker.getState());
}
}
@Test
void testTimeoutServiceCall() {
try (var executor = new FallbackExecutor()) {
// Primary takes 300ms, timeout limit is 50ms
var primary = new PrimaryService("Primary Response", 300, false);
var fallback = new FallbackService("Fallback Response");
var circuitBreaker = new SimpleCircuitBreaker(1, 1000);
String response = executor.execute(primary, fallback, circuitBreaker, 50);
assertEquals("Fallback Response", response);
assertEquals(SimpleCircuitBreaker.State.OPEN, circuitBreaker.getState());
}
}
@Test
void testCircuitBreakerOpenFastFail() {
try (var executor = new FallbackExecutor()) {
// Configure primary service to throw exception if called
var primary = new PrimaryService("Primary Response", 0, true);
var fallback = new FallbackService("Fallback Response");
var circuitBreaker = new SimpleCircuitBreaker(1, 1000);
// Force open by registering a failure
circuitBreaker.recordFailure();
assertEquals(SimpleCircuitBreaker.State.OPEN, circuitBreaker.getState());
// Now call execute. It should short-circuit and not call the failing primary (fast-fail).
String response = executor.execute(primary, fallback, circuitBreaker, 100);
assertEquals("Fallback Response", response);
}
}
@Test
void testCircuitBreakerRecovery() throws InterruptedException {
try (var executor = new FallbackExecutor()) {
var primary = new PrimaryService("Primary Response", 0, false);
var fallback = new FallbackService("Fallback Response");
// Failure threshold = 1, retry period = 100ms
var circuitBreaker = new SimpleCircuitBreaker(1, 100);
// Trip the breaker
circuitBreaker.recordFailure();
assertEquals(SimpleCircuitBreaker.State.OPEN, circuitBreaker.getState());
// Wait 150ms to exceed retry period
Thread.sleep(150);
// State should evaluate to HALF_OPEN
assertEquals(SimpleCircuitBreaker.State.HALF_OPEN, circuitBreaker.getState());
// Execute. Healthy primary service succeeds, state transitions to CLOSED
String response = executor.execute(primary, fallback, circuitBreaker, 100);
assertEquals("Primary Response", response);
assertEquals(SimpleCircuitBreaker.State.CLOSED, circuitBreaker.getState());
}
}
}
+1
View File
@@ -252,6 +252,7 @@
<module>backpressure</module>
<module>actor-model</module>
<module>rate-limiting-pattern</module>
<module>fallback</module>
<module>onion-architecture</module>
</modules>
<repositories>