Topics:
- What is a Null Object Pattern?
- How to apply
- How to implement
- Testing noncritical dependencies
- Conclusion
What is a Null Object Pattern?
Resilience of microservice architecture comes from its ability to quickly recover from failures and being able to service customers in the process of recovery. And all this should happen without significant degradation of customer experience. The Null Object Pattern helps to target the latter - ensure that customer experience doesn’t degrade much when a non-essential part of the system is not working. This pattern could be applied in many levels of the tech stack: starting from when a service for a frontend component is not available (see pic example with status bar) and finishing up when you can't get data for an object or a class within a (micro) service in the backend.
Pic 1. Example when failure of status-bar block causes service denial for whole web page
How to apply
- you need to come up with a list of web pages, app screens, API endpoints, etc which are crucial from a business perspective.
- fill a list of all external dependencies for each item that can cause a denial of service.
- decide if this dependency is actually critical for the user's intent (e.g. if failure of loading recommended items should block the checkout process).
- noncritical services that cause more failures first.
How to implement
Implementing this pattern is straightforward. If your method (such as a factory or repository) can not retrieve data for the object, simply return a null object. For the implementation in your language check out wikipedia and below you can see the diagram to get the idea:
Pic 2. Interface diagram of implementing Null Object pattern
However, blindly applying this pattern everywhere is a bad idea especially if the object's data is used later in modification queries... Imagine you have a repository that retrieves data for a user from the other service. If retrieval of the user fails, then your repository returns a null object with defaulted values (ID is set to 0, Name is set to “empty name” and so on). You can’t use it for writing queries otherwise your business logic will be inconsistent. So how to deal with this?
One of the options you have is to split your interfaces of the user object into two types: ones that could be implemented by Null Object and others that can’t. This separation is crucial to make sure that Null Object will never be used in writing queries.
Testing noncritical dependencies
It is relatively easy to implement for services with clear bounded contexts (e.g. recommendation block), however for commodity services like a user profile it isn’t that simple and requires a lot of hard work to turn it into a noncritical dependency. This happens because every request to the backend usually ends up with querying these services to render a name or other (in most cases usually) trivial information. To make sure that all this effort is not wasted it’s wise to enforce such behavior by introducing noncritical dependency testing.
One way to implement this type of testing is to make them a special case of your e2e regression test suite. The only difference is we re-route traffic for the dependency we want to artificially fail to a service that mocks failing behavior like responding with error or times out after 5 sec for example. See the example in the picture below:
Pic 3. Example of testing mocking user service with bad
behavior. Mock tests make a request with a header to turn off user-service. This header got propagated down the stack and when reaches user-service it’s got rerouted to mock with mocked behavior
For this implementation, we need a sidecar that reroutes traffic based on the request headers and the mock service which implements bad behavior.
Conclusion
Null Object Pattern is absolutely must have for well-scoped dependencies and at the same time could be a burden to implement for commodity ones as it would require a lot of engineering hours to implement.
Keep in mind that this pattern is just “hiding” the failures from the end user while others like circuit breaker, and health checks are actually trying to recover. So apply it when it’s necessary and don’t forget to set it up for each case when the null object pattern triggers.
Top comments (4)
I just read your article on using the Null Object pattern to improve user experience in microservices, and I must say it's truly insightful and inspiring! Your explanation of how the pattern can eliminate unexpected null pointer exceptions and provide a smoother user experience is spot on. It's incredible how such a simple design approach can have such a profound impact. Thank you for sharing this valuable insight and shedding light on how we can enhance our microservices architecture. Keep up the fantastic work!
Why not just use Optional?
Thanks for the question Mateusz
It's ok to do so but I see a couple of reasons on top of my mind why not to:
I mean Optional monad