Fallback Pattern in Microservices
The fallback pattern gives a service a planned alternative answer when a call to another service fails. Instead of passing an error up to the user, the caller returns something useful but less complete: a cached value, a sensible default, or a partial page. The goal is graceful degradation: the system keeps working with reduced features instead of failing completely.
This tutorial explains how the fallback pattern works, the main types of fallback, and when a fallback is the wrong answer. Three runnable Java 21 examples show a fallback chain, choosing fallbacks by failure type, and combining a fallback with a circuit breaker. Interview questions follow at the end.
1. What Is the Fallback Pattern?
A fallback is the code that runs when the normal path, usually a remote call, does not succeed. Think of a product page in an online shop. It shows the product details, the price, reviews and "customers also bought" recommendations, and each part may come from a different service. If the Recommendation Service is down, the page can show a fixed list of bestsellers instead. The customer may not even notice.
The fallback pattern is one of the resilience patterns. It usually works together with the timeout pattern, which decides when to stop waiting, the retry pattern, which tries again, and the circuit breaker pattern, which stops calling a failing service. The fallback decides what to return once those have given up.
2. Why Microservices Need Fallbacks
In a microservices system, failures are partial. At any moment, one of fifty services may be restarting, overloaded or unreachable, while the other forty-nine are fine. Without fallbacks, every page that touches the broken service fails, and a small problem in a minor feature becomes a full outage.
Fallbacks separate critical features from optional ones. Taking an order is critical; personalised recommendations are optional. Optional features should never be able to take down critical ones. With fallbacks, the shop keeps selling while the recommendation team fixes their service.
3. How the Fallback Pattern Works
- Try the primary call, with a timeout. If a circuit breaker is open, skip the call entirely.
- Classify the failure. A timeout, a 503 or a refused connection means the service could not answer, so a fallback may help. A 400 Bad Request, a 404 Not Found or "card declined" is a real answer, and must be passed on.
- Run the fallback chain in order of quality: for example, a fresh cached value first, then a safe default.
- Mark the response as degraded, so the UI can adjust and monitoring can count it.
4. Types of Fallback
4.1 Cached or Stale Data
Return the last good value, such as yesterday's exchange rate or a product description from five minutes ago. Decide how old is still acceptable. A product description can be a day old; a stock count for checkout cannot.
4.2 Default or Static Value
Return a safe, fixed answer: bestsellers instead of personal recommendations, standard delivery time instead of a calculated one, or "price unavailable" instead of a price. A default should never pretend to be real data.
4.3 Alternative Source
Call a backup: a second payment provider, a read replica of a database, or a simpler version of the service. This keeps full quality but adds another dependency that can also fail.
4.4 Partial Response
Leave out the failed part. The product page loads without the reviews section, and shows "Reviews are temporarily unavailable". This works well with the aggregator pattern, where one response combines several services.
4.5 Deferred Write
For a write, accept the request and finish it later: save the order as "payment pending" and retry the charge from a queue. This is honest, because the user is told the payment is pending, not that it succeeded.
4.6 No Fallback: Fail Fast
Sometimes the right fallback is a fast, clear error. If the Payment Service is down and you cannot defer the charge, say "Payment is temporarily unavailable, please try again in a few minutes". A quick, honest error is better than a slow timeout or a fake success.
5. Fallback Pattern in Java: Examples
These programs use only the JDK on Java 21, so the logic is visible. In a Spring Boot service, Resilience4j provides the same behaviour with annotations (section 6).
5.1 A Fallback Chain With Honest Results
The price comes live if possible, then from a cache if it is at most 30 minutes old, then as "unavailable". Each answer records its source:
import java.math.BigDecimal;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class FallbackChain {
// Every answer says where it came from, so the page can show it honestly
record PriceAnswer(String sku, Optional<BigDecimal> price, String source, boolean degraded) {}
record CachedPrice(BigDecimal price, Instant savedAt) {}
static final Instant NOW = Instant.parse("2026-10-28T10:00:00Z"); // fixed clock for repeatable output
static final Map<String, CachedPrice> cache = new HashMap<>();
static boolean pricingUp = true;
static BigDecimal callPricingService(String sku) {
if (!pricingUp) throw new IllegalStateException("pricing-service timed out after 800 ms");
return new BigDecimal("1499.00");
}
static PriceAnswer getPrice(String sku) {
try {
BigDecimal live = callPricingService(sku);
cache.put(sku, new CachedPrice(live, NOW)); // keep the last good value
return new PriceAnswer(sku, Optional.of(live), "live", false);
} catch (IllegalStateException e) {
CachedPrice cached = cache.get(sku);
if (cached != null && Duration.between(cached.savedAt(), NOW).toMinutes() <= 30) {
long age = Duration.between(cached.savedAt(), NOW).toMinutes();
return new PriceAnswer(sku, Optional.of(cached.price()), "cache, " + age + " min old", true);
}
return new PriceAnswer(sku, Optional.empty(), "default: price unavailable", true); // never invent a price
}
}
public static void main(String[] args) {
System.out.println(getPrice("KTL-17"));
pricingUp = false;
cache.put("KTL-17", new CachedPrice(new BigDecimal("1499.00"), NOW.minus(Duration.ofMinutes(12))));
cache.put("MUG-04", new CachedPrice(new BigDecimal("199.00"), NOW.minus(Duration.ofHours(5))));
System.out.println(getPrice("KTL-17"));
System.out.println(getPrice("MUG-04")); // cached value too old to trust
System.out.println(getPrice("TEA-500")); // never cached
PriceAnswer answer = getPrice("TEA-500");
System.out.println("Show Buy button? " + answer.price().isPresent());
}
}
PriceAnswer[sku=KTL-17, price=Optional[1499.00], source=live, degraded=false]
PriceAnswer[sku=KTL-17, price=Optional[1499.00], source=cache, 12 min old, degraded=true]
PriceAnswer[sku=MUG-04, price=Optional.empty, source=default: price unavailable, degraded=true]
PriceAnswer[sku=TEA-500, price=Optional.empty, source=default: price unavailable, degraded=true]
Show Buy button? false
The five-hour-old price is rejected, and no price is invented. Because the answer says it is degraded, the page can hide the Buy button rather than sell at a wrong price.
5.2 Choosing a Fallback by Failure Type
This is the mistake many tutorials make in their own examples: they catch every exception and return a fallback. That turns "card declined" into something the customer never sees:
import java.util.ArrayList;
import java.util.List;
public class FallbackByFailureType {
// Technical failure: the other service could not answer. A fallback may help.
static class ServiceUnavailableException extends RuntimeException {
private static final long serialVersionUID = 1L;
ServiceUnavailableException(String message) { super(message); }
}
// Business answer: the other service DID answer, with "no". A fallback must not hide it.
static class PaymentDeclinedException extends RuntimeException {
private static final long serialVersionUID = 1L;
PaymentDeclinedException(String message) { super(message); }
}
static final List<String> retryQueue = new ArrayList<>();
static String chargeCard(String orderId, String scenario) {
return switch (scenario) {
case "ok" -> "PAID";
case "down" -> throw new ServiceUnavailableException("payment-service: 503");
case "declined" -> throw new PaymentDeclinedException("card declined: insufficient funds");
default -> throw new IllegalArgumentException(scenario);
};
}
static String placeOrder(String orderId, String scenario) {
try {
return orderId + " -> " + chargeCard(orderId, scenario);
} catch (ServiceUnavailableException e) {
retryQueue.add(orderId); // a write fallback: accept now, charge later
return orderId + " -> PAYMENT_PENDING (" + e.getMessage() + ", queued for retry)";
}
// PaymentDeclinedException is not caught: the customer must see the real answer
}
public static void main(String[] args) {
for (String[] call : new String[][] {{"ORD-1", "ok"}, {"ORD-2", "down"}, {"ORD-3", "declined"}}) {
try {
System.out.println(placeOrder(call[0], call[1]));
} catch (PaymentDeclinedException e) {
System.out.println(call[0] + " -> error shown to customer: " + e.getMessage());
}
}
System.out.println("Waiting for payment retry: " + retryQueue);
}
}
ORD-1 -> PAID
ORD-2 -> PAYMENT_PENDING (payment-service: 503, queued for retry)
ORD-3 -> error shown to customer: card declined: insufficient funds
Waiting for payment retry: [ORD-2]
The technical failure gets a deferred-write fallback with an honest "pending" status. The business failure is not a failure of the system at all, so it reaches the customer unchanged.
5.3 Fallback With a Circuit Breaker, and Watching the Fallback Rate
Without a circuit breaker, every request waits for the full timeout before the fallback runs. With one, the fallback is returned immediately once the breaker opens:
import java.util.List;
public class BreakerWithFallback {
enum State { CLOSED, OPEN }
static State state = State.CLOSED;
static int consecutiveFailures = 0, primaryCalls = 0, fallbacks = 0;
static long waitedMs = 0;
static final int FAILURE_THRESHOLD = 3, TIMEOUT_MS = 800;
// Simulated recommendation service: down for this whole run
static List<String> callRecommendations() {
primaryCalls++;
waitedMs += TIMEOUT_MS; // each failed call costs a full timeout
throw new IllegalStateException("timeout");
}
static List<String> fallback() {
fallbacks++;
return List.of("Bestsellers: Steel Kettle, Tea Set"); // static, safe and cheap
}
static List<String> recommendations() {
if (state == State.OPEN) return fallback(); // fail fast: no call, no waiting
try {
List<String> result = callRecommendations();
consecutiveFailures = 0;
return result;
} catch (IllegalStateException e) {
if (++consecutiveFailures >= FAILURE_THRESHOLD) state = State.OPEN;
return fallback();
}
}
public static void main(String[] args) {
for (int request = 1; request <= 10; request++) {
List<String> shown = recommendations();
if (request <= 4 || request == 10) {
System.out.println("Request " + request + " (" + state + "): " + shown);
}
}
System.out.println("With the breaker: " + primaryCalls + " calls to the failing service, " + waitedMs + " ms spent waiting");
System.out.println("Without the breaker: 10 calls to the failing service, " + 10 * TIMEOUT_MS + " ms spent waiting");
double fallbackRate = fallbacks * 100.0 / 10;
System.out.printf("Fallback rate %.0f%% -> %s%n", fallbackRate,
fallbackRate > 20 ? "ALERT: users see degraded pages" : "ok");
}
}
Request 1 (CLOSED): [Bestsellers: Steel Kettle, Tea Set]
Request 2 (CLOSED): [Bestsellers: Steel Kettle, Tea Set]
Request 3 (OPEN): [Bestsellers: Steel Kettle, Tea Set]
Request 4 (OPEN): [Bestsellers: Steel Kettle, Tea Set]
Request 10 (OPEN): [Bestsellers: Steel Kettle, Tea Set]
With the breaker: 3 calls to the failing service, 2400 ms spent waiting
Without the breaker: 10 calls to the failing service, 8000 ms spent waiting
Fallback rate 100% -> ALERT: users see degraded pages
Users got bestsellers on every request, so nothing looked broken, and that is the danger. A good fallback can hide an outage for hours. Count how often fallbacks are used, and alert when the rate is high. A real breaker also moves to a half-open state after a wait, letting a few trial calls through to check whether the service has recovered.
6. Fallbacks in Spring Boot
With Resilience4j, you name a fallback method in the annotation, for example @CircuitBreaker(name = "pricing", fallbackMethod = "cachedPrice"). The fallback method must be in the same class, have the same return type and the same parameters, plus one extra exception parameter at the end. You can write several fallback methods with different exception types; Resilience4j picks the one whose exception type is closest to the thrown one.
Because the annotations work through Spring AOP proxies, they do nothing when a method calls another annotated method in the same class. The call must come through the Spring bean.
Other options: Spring Cloud OpenFeign clients can declare a fallback or fallbackFactory class when circuit breaker support is enabled, and Spring Cloud Gateway's CircuitBreaker filter can forward to a fallbackUri when a route fails.
7. Comparing Fallback Types
| Fallback type | Good for | Main risk |
|---|---|---|
| Cached or stale data | Descriptions, settings, exchange rates | Acting on data that is too old |
| Default value | Recommendations, delivery estimates | Looking like real data |
| Alternative source | Payments, critical reads | The backup fails too, or behaves differently |
| Partial response | Pages built from many services | UI must handle missing sections |
| Deferred write | Orders, notifications, emails | Needs a reliable queue and idempotent retry |
| Fail fast | Operations that cannot be faked or delayed | Users see an error, but quickly and clearly |
8. Common Mistakes and Pitfalls
- Falling back on business errors. Catching all exceptions hides "not found", "invalid input" and "declined", as section 5.2 shows.
- Fake success for writes. Returning "order confirmed" when payment did not happen creates angry customers and manual cleanup.
- Stale data for critical decisions. Using a cached stock count to accept an order can oversell.
- A fallback that calls another remote service without its own timeout. When both fail, the fallback makes things slower, not better.
- The fallback stampede. When the primary fails, all traffic suddenly hits the fallback. If the fallback is a database query that normally serves 1% of requests, it can collapse under 100%. Make fallbacks cheap, or protect them with a bulkhead.
- Untested fallbacks. Fallback code runs only during incidents, so it is the least tested code in the system. Test it on purpose, for example by stopping a dependency in a test environment.
- Silent fallbacks. Without metrics, nobody notices that a feature has been degraded for a week.
9. Best Practices
- Decide per feature whether it is critical or optional, and what a degraded version looks like, together with the product owner.
- Fall back only on technical failures: timeouts, 5xx errors, refused connections and open circuit breakers.
- Keep fallbacks simple, local and fast: in-memory data, a cache, or a static value.
- Be honest: mark degraded responses, show the age of cached data, and never fake a successful write.
- Combine with timeouts and circuit breakers, so the fallback runs quickly.
- Measure and alert on fallback rates per dependency.
- Test fallbacks regularly with failure-injection tests.
10. Summary
- The fallback pattern returns a planned alternative when a remote call fails, so the system degrades gracefully.
- Common fallbacks are cached data, defaults, alternative sources, partial responses, deferred writes and a fast, clear error.
- Fall back on technical failures only; pass business errors such as "declined" to the user unchanged.
- Use circuit breakers so fallbacks are immediate, and keep fallbacks cheap to avoid a stampede.
- A good fallback pattern is honest, measured and tested, because a silent fallback can hide an outage.
Fallback Pattern: Interview Questions
Common interview questions on the fallback pattern, from basic to scenario-based, with short answers.
Basic questions
Q. What is the difference between a fallback and a retry?
Answer: A retry tries the same call again, hoping a short glitch has passed. A fallback gives up on the call and returns an alternative answer. They are often combined: retry once or twice for temporary errors, then fall back if every attempt fails.
Q. Should the fallback live in the calling service or the called service?
Answer: In the caller. The fallback exists for the case where the called service cannot answer, so it cannot be the one to run it. The called service can still help by publishing data the caller can cache, so a good fallback is available.
Q. Can a fallback method throw an exception?
Answer: Yes. If there is no sensible alternative for a particular failure, the fallback can rethrow it or throw a clearer exception, such as a custom PaymentUnavailableException mapped to HTTP 503. A fallback is a place to decide what happens, and sometimes the right decision is a clear error.
Q. What is CallNotPermittedException in Resilience4j?
Answer: It is thrown when a circuit breaker is open and refuses a call without trying it. You can write a fallback just for this case, so you know the service was not even called:
Price cachedPrice(String sku, CallNotPermittedException e) { ... }
Intermediate questions
Q. How do you get the failure cause in an OpenFeign fallback?
Answer: Use a fallbackFactory instead of a plain fallback class. Its create(Throwable cause) method receives the exception, so the fallback can behave differently for a timeout and a 404.
@FeignClient(name = "pricing", fallbackFactory = PricingFallbackFactory.class)
Fallbacks require spring.cloud.openfeign.circuitbreaker.enabled=true.
Q. How do you add a fallback in Spring Cloud Gateway?
Answer: Add the CircuitBreaker filter to a route with a fallbackUri, for example forward:/fallback/orders. When the downstream service fails or the breaker is open, the gateway forwards the request to that local endpoint, which can return a friendly message or cached data.
Q. You use @Retry and @CircuitBreaker with a fallbackMethod on both. What can go wrong?
Answer: By default, Resilience4j's Retry aspect wraps the CircuitBreaker aspect. If the circuit breaker's fallback turns the error into a normal result, Retry sees a success and never retries. Put the fallback on the outermost annotation only, usually @Retry, so retries happen first and the fallback runs once at the end.
Q. How do you write a fallback with WebClient?
Answer: Use Reactor's error operators on the response, catching only the errors that deserve a fallback:
webClient.get().uri("/recommendations").retrieve().bodyToMono(Recs.class).timeout(Duration.ofMillis(500)).onErrorResume(TimeoutException.class, e -> Mono.just(Recs.BESTSELLERS))
Q. How do you test fallback behaviour?
Answer: Make the dependency fail on purpose. WireMock can return 503 or add a delay longer than your timeout, and Toxiproxy can cut or slow a real connection. Assert that the response contains the fallback, is marked as degraded, arrives within the expected time, and that the fallback metric increased.
Tricky and scenario questions
Q. The Inventory Service is down during checkout. Should you use the cached stock count?
Answer: Usually not for the final decision, because stale stock data can oversell. Depending on the business, you can fail fast with a clear message, or accept the order as "pending confirmation" and confirm or cancel it once stock is checked. Cached stock is fine for display, such as "in stock" on a product page.
Q. Your fallback reads from Redis, and Redis is down too. What happens?
Answer: If the cache call has no timeout, the fallback hangs and the request becomes even slower. Give the cache call a short timeout, and add a final step to the chain, such as a small in-memory copy or a static default. The last step of every fallback chain should depend on nothing remote.
Q. A fallback for "my orders" returns an empty list. What is wrong with that?
Answer: An empty list looks like a real answer: "you have no orders". Customers may panic or order again. The response must say "orders are temporarily unavailable", for example with a degraded flag, so the UI can show the difference between empty and unknown.
Q. After the Recommendation Service went down, the product database hit 100% CPU. Why?
Answer: The fallback probably queried the database for top-selling products on every request. Normally few requests used that path; during the outage, all of them did. Precompute the bestseller list, keep it in memory and refresh it periodically, so the fallback costs nothing extra under load.
Q. How do you know a fallback is hiding a real problem?
Answer: Track fallback calls per dependency as a metric, for example a Micrometer counter tagged with the reason, and alert when the rate stays above a threshold. Also log the underlying error once per occurrence, with the trace ID, so the team that owns the failing service can see it.