Architecture for Scale Part III: The Monolith

Architecture for Scale Part III: The Monolith

In recent years microservices have been very much in vogue. Like any architecture, used in the right conditions, they have obvious benefits. Microservices offer fine-grained scalability, technological heterogeneity and easy fault isolation. However, like anything else they also come with challenges. There is an increased operational overhead to factor in, distributed system complexity such as managing transactions across services and increased development effort, particularly in the planning and design phase of a project. Perhaps the biggest victim of this recent trend has been the humble and long in the tooth monolith. As the adoption of microservices has grown in recent years, the popularity of monolithic architectures has declined by a similar order of magnitude in the opposite direction. Thankfully we seem to be trending back towards a more pragmatic opinion of monolithic architectures as evidenced by this excellent piece[] by prime video tech. This blog piece explains what monoliths are, when you should adopt this type of architecture and the pros and cons of doing so. It also explains how monolithic architectures can also be built to scale if and when needed to.

First off let’s refresh our memories as to what a monolith actually is. A monolithic architecture is a traditional software architecture style whereby an application is built as a single, self-contained unit, deployed as a single running process (scaling requirements can muddy this a little bit I guess). A monolith typically has a single codebase/repository and a single database. The obvious advantages of a monolith are; simplicity, monolithic architectures are straightforward to develop and deploy; code reuse, since all code resides in a single codebase it’s easier to share and reuse across different modules and components within the application; performance, with direct access to shared memory and resources, overheads like network latency are not an issue.

Monoliths are not without their drawbacks however. It is usually non-trivial to scale a monolithic application. The entire application must be scaled regardless of the fact that certain components may have higher resource demands than others. As the codebase grows a monolithic architecture can become overly complex and difficult to maintain, particularly if the code hasn’t been designed well. In an opposite vein to microservices monoliths are not technologically heterogeneous and experimenting with different development languages within the same application is in most cases not possible.

The most considerable of these drawbacks is the ability to scale easily. When we are faced with a monolithic application that needs to scale beyond it’s current capabilities we have three possible options. We can scale vertically. This means simply increasing the resources available to the application in terms of CPU, memory, storage, etc. This approach is fine for moderate scaling increases however in the medium to long term you could hit a ceiling in terms of horsepower and/or costs. The second approach is to scale horizontally. Scaling horizontally means deploying additional instances of the application and spreading the load across these instances. This approach makes more sense than scaling vertically when you need to scale by orders of magnitude and/or on-demand. Horizontal scaling also improves the applications fault tolerance overall in that there is no longer a single point of failure; if one instance fails the other instances can serve requests while the failed instance is being brought back up by the orchestrator. Both of these approaches are infrastructural approaches to scaling. The third option involves software change as well as infrastructural change. This approach to scaling involves starting to break functions of the monolith up, leveraging something like the strangler pattern to move functionality from the monolith to microservices. This approach is one that you should probably take when the scaling requirements of your features within the monolith differ significantly and there are unnecessary cost overheads incurred by having to scale the entire application uniformly. This method of scaling is significantly the most difficult.

The key to being able to scale in this way is designing and arranging your code properly. You should aim for a code base that is exhibits high cohesion and loose coupling. Highly cohesive code is code that is divided into modules whereby elements that depend on each other exist within the same module. Loosely coupled code is when we ensure classes have a single responsibility and have separated concerns, typically achieved via abstraction. One way to achieve the above is to follow a modular monolithic architecture. This pattern involves breaking the monolith up into separate modules each within their own bounded context. This allows us to easily lift and shift the code into a dedicated microservice should scaling requirements dictate that approach in the future. A codebase with no apparent structure or design will make it virtually impossible to strangle features from the monolith and more often or not it is actually easier and more timely to simply build the feature again from scratch as a microservice.

Monoliths overall are a very effective and underrated architectural approach. There are many contexts in which they make complete sense and offer obvious benefits over more complex architectures such as a microservices approach. No one situation is the same but we tend to guide our clients down the monolith path when we are dealing with early stage start-ups in particular, when they need to get something to market quickly and start generating revenue. It makes no sense in such scenarios to try and build the fine-grained scaling microservices offer, and burden our clients with the complexity that comes with that, before it’s required. Clients light on operational resources and expertise are also more typically suited to monolithic applications in our experience. The key thing we always endeavour to achieve when we are building a monolith for a client is to do so in a modular fashion. This allows us to be agile and pivot quickly when the client enters the scale up phase in their growth and we can then easily strangle out the required features in the monolith and drop them into microservices, coarse grained services or something in between.