The Gay Monolith Pattern for Closeted Microservices
Table of Contents
Every application proposal trying to win the heart of a Fortune 500 company today not only needs to claim to be Agile—oh, sorry, SAFe—but also based on a microservices architecture. After high-profile microservices disasters like Dell’s, we understand that a myopic microservices approach may not end up well. What if we could have all the benefits of a microservices architecture but none of the drawbacks? Is this even possible? Maybe: enter the Gay Monolith architectural pattern.
In the good design patterns style, let me start off with the problem statement first and the solution later.
Microservices are isolated network components with their own independent deployment and runtime life cycle. The challenge is that it is impossible to tell in advance how many microservices are—or should be—required. Even though bounded contexts may help identify microservices, it is not true that all relevant bounded contexts will be known at the start of the project. It is neither certain that there will be a 1:1 mapping between bounded contexts and two pizza teams: a single developer may work on two or more different bounded contexts especially in the early stages. The problems that arise as a result of churning out microservices in an uncontrolled manner are multiple fold:
- High latency: the latency profile for the emerging set of microservices may be unsustainable. Two or more functions need to be placed together to avoid excessive network trips.
- Developer productivity decline: The burden of managing the orthogonal life cycles of each microservices may be excessive. Every functional aspect lives in a specific microservice that needs to be located and managed independently. Moreover, a simple change in the general build process as well as general refactoring change (e.g. use of tabs, camel case, etc.) needs to be applied on a one-by-one basis to each microservice project folder.
- Complex runtime platform: Managing more than a handful of microservices without an orchestration platform such as Kubernetes, Docker Swarm or PCF becomes not only labour intensive very soon, but highly error-prone as well.
- Clear-cut bounded contexts are not evident.
- The initial team is small—even a single developer.
- The load profile (what function points are likely to receive the most load) cannot be easily anticipated.
A gay monolith has the external appearance of a regular monolith (single deployment, execution under one stack address space, and so on) except that, in reality it hides a set of closeted microservices waiting to come out whenever the conditions are appropriate. A gay monolith follows three fundamental rules:
- The initial gay monolith has one build and deploy process.
- General bespoke utility or helper functions (for example, libraries to connect to a Foreign Exchange (FX) and Shipping services) must be implemented as a different project with their own build process. Third party libraries (e.g. Flask) fall under this definition by default.
- Bounded contexts (e.g. Catalogue, Order, and Fulfilment) have no interdependencies:
- All entry points to the bounded context are grouped together at the top level.
- The implementation of the bounded context lives in a discrete module or package.
- The implementation of the bounded context has no references to the top-level module/package.
- The implementation of a given bounded context has no references to other bounded contexts.
- If using a SQL RDBMS, JOINS are restricted to the bounded context at hand.
When to Let Microservices Come Out of the Closet
Once that we have a gay monolith, a key question is what are the conditions that suggest that a given bounded context must come out and become an independent microservice. Broadly speaking, the conditions are as follows:
- Scalability: A bounded context has relative load and scaling profile higher than other bounded contexts. For example, the Catalogue service may experience a load of 300TPS as opposed to, say, 100TPS, which is the load experienced by other bounded contexts.
- Frequency of Change and Deployment: If one bounded context changes much more frequently than others (resulting in more frequent releases as well) then it makes sense to isolate the volatile parts from those that are more stable.
- Degree of Development Focus: A bounded context is increasingly managed by a separate two pizza team or dedicated developers. This may be measured as the number of man days dedicated to a specific bounded context.
Creating an Independent Microservice Out of a Gay Monolith
The first step consists in duplicating the gay monolith repo, and then applying the following changes to the cloned gay monolith:
- Removing unrelated invocations to other bounded contexts from the top microservices entry point.
- Removing the packages/modules applicable to the unrelated bounded contexts.
- Removing unrelated library imports from the build process.
The second step is removing all references to the bounded context that now lives in a separate microservice from the gay monolith so that we reduce its complexity and avoid leaving technical debt behind. Finally, we have to change any consumers' endpoints so that they point to the new microservice as opposed to the monolith. This may not be necessary as explained further on.
Challenges and Solutions
Identifying the Bounded Contexts in the First Place
The whole rationale for the Gay Monolith pattern is that it is very hard to pin down what are the clear-cut bounded contexts at the beginning of the project. Thus, by having a single code base mounted on an IDE at the very beginning, moving functionality around is not a problem. The idea is that the bounded contexts will emerge organically as opposed to being shoehorned into ill-conceived, premature microservices.
URI Pollution and EndPoint Changes
Let us imagine we have two bounded contexts: RetailCatalogue and CorporateCatalogue. If both of them have a
/browse URI, then we may have a clash which may lead us to define verbose URIs in the Gay Monolith such as
/browseCorporateCatalogue. If this is undesired, then a virtual hostname approach may be used.
Using virtual hosts implies that the microservices need to be context aware. In the case of HTTP-based ones, this means that they need to be aware of the hostname used for the invocation. For example:
1match host with 2 "retail.company.com" -> 3 match uri with 4 /browse -> RetailCatalogue.browse() 5 "corporate.company.com" -> 6 match uri with 7 /browse -> CorporateCatalogue.browse()
The above pseudocode requires that aliases are defined for the same gay monolith. As a bonus, this approach allows stripping the microservices off the gay monolith in a transparent manner—as far as service consumers are concerned—since it only involves replacing the host aliases with actual DNS A records.
The Gay Monolith pattern is just common sense and has probably been described in numerous ways by a plethora of practitioners. My aim here was not claim any form of original thinking but just—in a spirit agianst bigotry—to come out with a cool metaphor in regards to how we ought to think about starting our microservices journey.
Monoliths is what we all start with and there is nothing wrong with them. But just by making them a little gay, we can have a development model we understand without introducing unnecessary complexities until we need them.