Avoiding the Microlith Anti-pattern in a Microservices Architecture
Daniel Graham
Computer Scientist | Full Stack Staff Software Engineer | Software Architect (views expressed are mine and mine alone and do not represent any employer or their affiliates)
In the year 2024, almost every software engineer has by now familiarized themselves with the concepts of microservices and perhaps even the tenants of a microservices architecture. If one is old enough, they may even be aware of Service Oriented Architecture (aka SOA) from which microservices are based and its four main tenants:
In our SOA evolution and maturation into microservices, some might be led to believe that we’ve abandoned these tenants, and that might could be understood given how these tenants were articulated early on compared to our knowledge and understanding of how JSON-based / RESTful microservices operate…but I’m going to suggest that the spirit of the law remains unchanged even if the letter of the law needs updated in a couple of cases.
In this post, I’m going to focus on the first tenant of SOA and how that translates to microservices architecture despite it being a somewhat common violation (although thankfully this is becoming less the case from what I have observed in the more established / mature shops).
Setting Unhealthy Boundaries
One of the easiest ways I’ve found to talk about this tenant is to illustrate the most common way in which it’s violated. There are more obscure ways where this can be a problem (e.g. god classes), but this anti-pattern typically emerges when we’re dealing with multiple microservices and the problem of composition.
The following is a high-level illustration of this anti-pattern. This is called the microlith pattern as a portmanteau of the words microservice and monolith as both patterns are inadvertently being expressed. The anti-pattern emerges from the fact that the tactical implementation is a microservice, yet because of the tight-coupling via the direct call to another microservice, most if not all of the value of the microservice architecture is lost and one is left with the same essential value of a monolithic architecture (but with the premium management overhead costs of the microservice).
Notice how the East / West traffic is flowing directly between microservices, which expresses this anti-pattern design. When we say that the value of the microservices architecture is lost here, concretely we can say the following:
While there may be examples of doing this in articles or even products sold that seek to tap into your microservices workflows in this manner, it should be clear to see that this is nevertheless an anti-pattern with formidable downsides.
Services Have Strict Boundaries: It’s All In The Directions
While there are several means in which to solve the above problem and ultimately avoid the microlith anti-pattern…a grossly oversimplified rule of thumb in order to do so could be summarized as this:
East / West traffic should never call a microservice…only North / South traffic.
Now that might sound odd or perhaps even just idealistically simple, and fair enough…but if and when you test the various means of best practices regarding the leveraging of multiple microservices in a composed manner, this rule largely holds true.
Let’s observe one of the most common ways in which we can apply this principle and swap the above call wholesale without disrupting the caller in any way. In other words, what can we do if we need to have a synchronous call to a microservice (e.g. from a micro-frontend website application) but still adhere to the SOA tenant of strict boundaries and yet have that call return data from Microservice A in the same manner as above where it’s enriched with data from Microservice B and C? This is where a mature microservices architecture typically incorporates a message bus architecture into its stack and will engage in a pattern generally known as Enterprise Application Integration:
Notice here how none of the East / West traffic actually touches a microservice…rather that direction is flowing through the message bus. Only North / South traffic is touching a microservice. In this model, the request to Microservice A never has to leave its own database in order to fulfill that request as all of the non-sovereign data from Microservice B and C that is needed has been copied to the message bus and cached locally in Microservice A’s database. This allows us to stay aligned with the Database Per Service pattern / best practice and allows us to retain all of the value associated with a microservices architecture. Not only can we independently scale and deploy each microservice, but if one particular service is down, it doesn’t affect the others…even when there’s a data dependency between them (e.g. If Microservice A has the latest data synced to its local cache from Microservice B, and Microservice B goes down, then no new changes are going to be incoming while Microservice A can continue to remain operational).
领英推荐
Some of the challenges that one must contend with however are of course the eventual consistency model that such data caching imposes. Even if we are to get the latency to a near-realtime scenario (which is fairly easy using modern platforms) we should nevertheless understand that it is not immediate and ensure that our business use cases can align with this. We must also account for an initial hydration of the cached data, but this too can be fairly trivial. There are of course many other ways one can imagine in which to avoid the microlith anti-pattern by adherence to the East / West traffic rule (e.g. having the initial caller receive an async token while each microservice is called using an async mechanism which is eventually resolved in a callback using said async token). Whichever methodology one conceives, each one involves specific tradeoffs that must be evaluated against the use cases with the best alignment carefully considered.
Exceptions: Don’t Fall For It!
Like most things in architecture, there are very few absolutes (although not 0), and anytime something is labeled an “anti-pattern”, there’s usually that one scenario, however rare it may be, where it ceases to be an actual anti-pattern and just becomes the pattern. While there are some products that will be happily sold to you along the lines of encouraging an exception to adopt a microlith, the following is nevertheless NOT one of these rare occasions.
“IAM Authorization is the one time that it makes sense to allow a microservice to call into another microservice because we want to centrally manage our IAM roles and permissions”.
The previous quote is pernicious on at least two major levels:
Ultimately a proper OAuth2 implementation isn’t meaningfully different than the EAI pattern, at least not in terms of the idea that in having a separate / centrally-managed application for your IAM, this data will have to get synced to your Authentication Provider / Auth Server in some manner similar to caching it into a consuming microservice's datastore. If you use your Authentication Provider / Auth Server to manage your IAM, then you can technically save yourself this step. The main difference is simply where the data is located and thus retrieved, but the effect is all the same in that the downstream microservices don’t have to leave their own boundaries in order to access this IAM data as it will be in the incoming JWT bearer token.
Consider a SaaS application that uses Okta to authenticate users leveraging its OIDC authentication services…and since we’re talking a web application, we’ll say we’re using the Authentication Code with PKCE workflow here. Given that OIDC is only designed for Authentication (and not Authorization), it’s nevertheless built on top of OAuth2 which is designed for Authorization, and so we can envision a separate and centralized application to manage IAM roles and user permissions and express our microservices architecture starting with the following illustration:
So now that we have our IAM data in Okta stored in the appropriate section of its User API, let’s further say that we have an Angular application that has the calls to our microservice endpoints located in various Angular services which are used by various page components. For each page that a user navigates to, our Angular OIDC library will take care of exchanging our OIDC authorization token for an OAuth2 access token, thus requesting the specific scopes that are germane for that page / data service. This resultant OAuth2 access JWT token will have the synced IAM data that our microservice will need, thereby allowing the microservice to NOT have to make any external calls to fetch this data.
Conclusions
The microlith anti-pattern can be a detrimental and costly mistake that seeks to rob you of your investment in a microservices architecture. Don’t let yourself be convinced that this is acceptable a little or in some cases simply because of the existence of products that rely upon this anti-pattern or because so many times in architecture there are exceptions to the rule and so it feels like this should be no different. This isn’t to say that there may not be an extremely compelling case where this is just simply unavoidable, but that still doesn’t detract from the reality that this should generally be avoided as the case default and defended with as much effort as possible. When your product is in full swing and anything that keeps complexity low is your friend, your support teams will thank you as will your ROI and TCO!
Happy Building!
<daniel />
VP, Software Engineering | Leadership | Management | Cloud Computing | Healthcare | Accounting | Finance
1 年Nice to see this, Daniel ??