Lately I write a lot about service resiliency and this time I’ll go with the same topic.
A couple articles back, I showed you how to test and integrate circuit breaking into your Spring Cloud Feign clients using Resilience4J as well as how to deal with TimeLimiters from the Resilience4J portfolio.
Today, I’ll show you how to improve the behavior of your app with fallbacks when a service is crashing, or unavailable either due to issues with the service itself or because a circuit breaker or a time limiter cuts the traffic off.
Concept of fallbacks
The concept of fallbacks is easy to understand. In simple terms, it’s a custom implementation that will come into play when something is failing.
Practical use-case. Let me put the usual architecture diagram here:
When the online-store-service
is trying to talk to the user-session-service for session validation and the user-session-service
is unavailable or just responding with HTTP 500 Internal Server Error, you don’t want to cascade that error to your public API, right?
You have two choices. Either you wrap your user-session-service
Feign client call into a try-catch and manually try to do something. Or you create a fallback implementation for the user-session-service
.
Since the service is failing for some reason and the API is only validating whether a session is valid, doesn’t it make sense to say the session is invalid when an error occurs?
I mean from a public API user point of view, you can’t access the resources either way so why don’t we fail gracefully?
Configuring Feign for fallbacks
We’ll do just that. The thing is, Spring Cloud OpenFeign provides a handy way to configure fallbacks. Namely, there’s a fallback
attribute on the @FeignClient
annotation but there are a couple of things you should be aware of.
Let’s make our hands dirty and look at some code:
@FeignClient(name = "user-session-service") public interface UserSessionClient { @GetMapping("/user-sessions/validate") UserSessionValidatorResponse validateSession(@RequestParam UUID sessionId); }
Simple Feign client for describing the user-session-service
API, nothing special.
Since the Spring Cloud OpenFeign package doesn’t support fallbacks by default, we have to involve another Spring Cloud package, circuitbreaker.
To the build.gradle
/pom.xml
:
// omitted ext { set('springCloudVersion', "2020.0.4") } dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j' // omitted for simplicity } dependencyManagement { imports { mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" } } // omitted
Next up, we gotta enable the circuit breaker for the Feign clients.
application.properties
feign.circuitbreaker.enabled=true
And now we have the capability to set up fallbacks for our Feign clients.
Implementing Feign fallbacks
One way to implement a fallback for a Spring Cloud Feign client is to create a class that implements the Feign client interface and register it as a Spring bean and specify it in the fallback
attribute of the @FeignClient
annotation.
Let’s see it in action for the UserSessionClient
.
@Component @Slf4j public class UserSessionClientFallback implements UserSessionClient { @Override public UserSessionValidatorResponse validateSession(UUID sessionId) { log.info("[Fallback] validateSession"); return new UserSessionValidatorResponse(false, sessionId); } }
Just for completeness, here’s the API response POJO:
@Data @NoArgsConstructor @AllArgsConstructor public class UserSessionValidatorResponse { private boolean valid; private String sessionId; }
In the fallback implementation, I hardcoded that the valid
flag within the response is always false, meaning that whenever the fallbacks comes into play, we’ll simply say that the session is invalid, since we cannot validate it through the user-session-service
.
Register it on the Feign client:
@FeignClient(name = "user-session-service", fallback = UserSessionClientFallback.class) public interface UserSessionClient { @GetMapping("/user-sessions/validate") UserSessionValidatorResponse validateSession(@RequestParam UUID sessionId); }
That’s it, easy right?
Let me add one more note here. If you’re using Spring on a daily basis, you might ask, how the hell does Spring distinguish between the fallback implementation of the UserSessionClient
interface and the actual Feign client which is created as a runtime proxy. Now we’ll have 2 Spring beans for the same interface type and when we’re autowiring the UserSessionClient
into our beans, it magically resolves it to the proper bean. How?
Well, it’s a tiny trick hidden from you. When Spring Cloud creates a Feign client from the interface, it marks the client’s proxy as @Primary
, telling Spring to always inject this bean if it’s uncertain which implementation to use.
Let’s try it by not starting up the user-session-service
or by sending back an HTTP 500:
... 2021-11-12 11:11:11.250 INFO 15652 --- [pool-1-thread-1] c.a.o.s.s.e.s.UserSessionClientFallback : [Fallback] validateSession ...
The fallback’s log message is triggered and the resulting response contains what we set in the fallback. Awesome.
Using a fallbackFactory
Another way to define fallbacks is through the fallback factory which – you guessed it right – creates fallback instances each time an error happens.
What’s the difference between this and the regular fallback? With the regular fallback, you can’t access the underlying exception that triggered the fallback however with the fallback factory, you can.
So in case you need to customize the logic of your fallback based on the exception, you can totally do it using a fallback factory.
We need to implement the FallbackFactory<T>
interface where T represents the type of the Feign client, in this case UserSessionClient
.
@Component @Slf4j public class UserSessionClientFallbackFactory implements FallbackFactory<UserSessionClient> { @Override public UserSessionClient create(Throwable cause) { log.error("An exception occurred when calling the UserSessionClient", cause); return new UserSessionClient() { @Override public UserSessionValidatorResponse validateSession(UUID sessionId) { log.info("[Fallback] validateSession"); return new UserSessionValidatorResponse(false, sessionId); } }; } }
Let’s configure it through the @FeignClient
annotation’s fallbackFactory
attribute.
@FeignClient(name = "user-session-service", fallbackFactory = UserSessionClientFallbackFactory.class) public interface UserSessionClient { @GetMapping("/user-sessions/validate") UserSessionValidatorResponse validateSession(@RequestParam UUID sessionId); }
Then see it in action, this time the user-session-service
is returning an HTTP 500:
... 2021-11-12 11:19:06.011 ERROR 17624 --- [pool-1-thread-1] s.s.e.s.UserSessionClientFallbackFactory : An exception occurred when calling the UserSessionClient feign.FeignException$InternalServerError: [500 Server Error] during [GET] to [http://user-session-service/user-sessions/validate?sessionId=828bc3cb-52f0-482b-8247-d3db5c87c941] [UserSessionClient#validateSession(UUID)]: [] at feign.FeignException.serverErrorStatus(FeignException.java:231) ~[feign-core-10.12.jar:na] at feign.FeignException.errorStatus(FeignException.java:180) ~[feign-core-10.12.jar:na] at feign.FeignException.errorStatus(FeignException.java:169) ~[feign-core-10.12.jar:na] at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:92) ~[feign-core-10.12.jar:na] at com.arnoldgalovics.online.store.service.external.error.HandlingFeignErrorDecoder.decode(HandlingFeignErrorDecoder.java:65) ~[main/:na] at feign.AsyncResponseHandler.handleResponse(AsyncResponseHandler.java:96) ~[feign-core-10.12.jar:na] at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:138) ~[feign-core-10.12.jar:na] at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:89) ~[feign-core-10.12.jar:na] at org.springframework.cloud.openfeign.FeignCircuitBreakerInvocationHandler.lambda$asSupplier$1(FeignCircuitBreakerInvocationHandler.java:112) ~[spring-cloud-openfeign-core-3.0.4.jar:3.0.4] at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na] at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na] at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na] at java.base/java.lang.Thread.run(Thread.java:829) ~[na:na] 2021-11-12 11:19:06.013 INFO 17624 --- [pool-1-thread-1] s.s.e.s.UserSessionClientFallbackFactory : [Fallback] validateSession ...
Awesome, works like a charm and we could customize the fallback logic based on the exception.
The fallback gets triggered when there’s an issue with the service, if the circuit breaker is open or even if a time limiter cuts off the traffic. It’s a good way to make your services behave as you want, even in cases when there’s an error. And trust me, it’s better to be prepared for errors and handle them gracefully than errors hitting you in the … in production.
Good luck and have fun with fallbacks. If you’re curious about other aspects of Feign, be sure to check out my Mastering microservice communication with Spring Cloud Feign course.
If you liked the article or you think it’s useful, be sure to share it with your friends, colleagues, fellow developers and if you wanna see more things from me, follow me on Facebook and Twitter.
When imlpementing this code, the object is returned from FallBack even when the HttpStatusCode is 200.
Hi Bruno. That’s a strange behavior. Would you mind opening a SO question with some code that reproduces the issue? Thanks!
I created an example project to demonstrate the problem (https://github.com/bcmes/circuitbreaker-with-feignclient.git). When making a call that returns 200 OK (curl –location –request GET ‘localhost:8080/characters/10’) it triggers the fallback. I expected the fallback to be triggered only when the call returned 404 (curl –location –request GET ‘localhost:8080/characters/101’).
Note: This is my first time using this fallback feature !
Hi Bruno. Thanks for the example project. I checked it.
Turns out the API you’re using is exceeding the default timeouts set by Resilience4J. If you disable the fallback temporary, you’ll get the following exception:
java.util.concurrent.TimeoutException: TimeLimiter ‘Swapi#people(int)’ recorded a timeout exception.
Hence the fallback will trigger every time despite the fact that you see the API response in the logs (since you’ve enabled Feign logging) but don’t get confused. If a timeout happens and the traffic gets cut off by Resilience4J, the response can arrive from the server afterwards and it’ll be logged by Feign.
Hope that helps.
Arnold
Thank you very much Arnold Galovics,
Now I understand why, I really was getting confused, thank you very much for your help!
You’re welcome. 🙂
Excellent guide. Worked like a charm, thanks!
Thanks Robert. Glad it helped. 😉
How to use @CircuitBreaker with @FeignClient?
Hi Robert,
Could you please be a little more specific? If you’re generally interested in circuit breaking with Feign clients, I suggest checking out my Mastering microservice communication with Spring Cloud Feign: https://www.udemy.com/course/mastering-microservice-communication-with-spring-cloud-feign/?referralCode=437693FE520ABDF6F4E3
Cheers.
it is not working for me,it is not calling fallback method
i did the following:
@Component
@Slf4j
public class SessionFallback implements Myfeign {
@Override
public ResponseEntity adding(AddDTO addDTO) {
throw new NotDoundException(“Im down”);
}
}
feign interface:
@FeignClient(name = “my-service”, fallback = SessionFallback.class)
public interface Myfeign {
@PostMapping(value = “/here”)
@ResponseBody
ResponseEntity adding(@Valid @RequestBody AddDTO addDTO);
}
Hi there,
Do you have an example project on GitHub that reproduces the issue?
Arnold
How can we integration testing and get the fallback Information
Hi Mick,
Could you please be more specific with your question?
Arnold
Is there a way to access the HTTP Code that fired the fallback?
Hi Ariel,
If you’re using a FallbackFactory, you have access to the exception that caused the fallback to trigger. If I remember correctly the actual exception implementation is a FeignException which allows you to get the HTTP code.
Hope that helps.
In latest spring boot version(s), property is changed to:
“spring.cloud.openfeign.circuitbreaker.enabled=true”