Avoiding the Microlith Anti-pattern in a Microservices Architecture
A microservices DeathStar lurks at the top of your article!

Avoiding the Microlith Anti-pattern in a Microservices Architecture

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:

  • Services have explicit boundaries
  • Services are autonomous
  • Services share schema / contract (not class)
  • Service compatibility is based upon policy

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).

Microlith Anti-pattern

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:

  • Changes to Microservices B and/or C will most likely require upstream changes to Microservice A, and so the ability to be independently deployable has been severely compromised. This level of coordinated deployment is part of what can make monoliths expensive and difficult to maintain over time.
  • Due to this violation of service boundaries, each hop between the microservices not only generates additional network traffic and therefore potential latency, but will generally incur a serialization / deserialization cost at each call point.
  • This network traffic and latency can become further exacerbated if there is security authorization that must take place between each service call (e.g. Client Credentials flow using an OIDC or STS service).
  • The serialization / deserialization costs can also become even more expensive when coupled with potential TLS encryption / decryption that may be required in order for traffic to flow across the network.
  • This design also disallows Microservice A to offer its own SLA as anytime Microservice B or C are down for any reason, Microservice A will also be down.

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:

Microservice Architecture: EAI Pattern

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:

  • IAM Authorization is generally going to affect every call to every microservice.
  • If the EAI-based pattern isn’t desirable, there is an entire open-source authorization protocol that you’re probably already using to secure your microservices (and if not, you probably should be), and this is OIDC / OAuth2.

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:


Microservices Architecture: EAI Pattern - Centralized IAM


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.


OIDC / OAuth2 with authorization data in JWT access token

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 />

Ramesh Venkatakrishnan

VP, Software Engineering | Leadership | Management | Cloud Computing | Healthcare | Accounting | Finance

1 年

Nice to see this, Daniel ??

要查看或添加评论,请登录

社区洞察

其他会员也浏览了