Avoiding Monolithic Regret: Moving to Microservices
The last year has seen a considerable spike in the number of articles and discussions around microservices. Many think that a microservice-based architecture is the right answer for their organization, whether they are a startup or a large-scale enterprise. Others are opting for a wait-and-see approach to microservices by first adopting a monolithic architecture.
A microservice architecture provides the opportunity to create smaller, loosely coupled services that focus on doing a small number of tasks well. The benefits to developers include the ability to focus on a smaller codebase, more isolated deployments that can occur more often, and less overall impact to the codebase when updating or replacing microservices as requirements change. The business also benefits by having a more composable business for greater flexibility and agility as new market opportunities emerge.
Whether you choose to build a monolith, or start with a monolith on your way to microservices, the best of intentions could still lead to problems down the road. Let’s examine how this can happen and how to prevent these kinds of monolith problems.
What is so wrong about a monolithic architecture?
Monolithic applications are popular because they are the easiest to construct. All of the code exists within a single codebase. Therefore, the code interacts within the same process, removing the need for network latency and failures common with distributed architectures.
However, teams may experience difficulties with monolithic applications as the codebase grows. Problems such as loss of agility over time, features requiring longer development time, and a fragile codebase. This is what I have termed monolithic regret.
What causes monolithic regret?
The majority of teams experience monolithic regret as a result of a lack of clear software architecture and boundaries. This may be the result of limited time, lack of focus, or the heavy dependence on a web framework that doesn’t encourage proper cohesion and coupling.
Software is the most flexible when we have a strong cohesion and loose coupling. What does this mean?
Strong cohesion means that code related to the same functionality is grouped together. Code modules that have a strong cohesion promote reuse and prevents code changes from impacting many areas across an entire code base.
Loose coupling means that the dependency between modules is limited, preventing modules from knowing the internal details about how they are implemented. This is typically accomplished through clear public APIs for a module.
There are several popular web frameworks that make building a monolithic application easy – almost trivial. These frameworks commonly offer accelerated development, code generation, open source plugins/extensions, simplified deployment and the ability to scale out by adding more instances as required to support the current load. The problem is that they encourage poor software architecture unless carefully managed. Below is an example layout of the directory structure for a Ruby on Rails application:
The problem I have encountered is that the opinionated directory layout and associated boilerplate generators lend themselves to monolithic regret for more complex applications. These frameworks have enabled many developers to leave behind complex frameworks, sparking fun and innovation across a variety of technology and programming language communities. But it has also led to a loss of focus on strong cohesion and loose coupling in our solutions.
Can you get around these sorts of problems with these frameworks? Absolutely. The problem is that these frameworks don’t default to a modular approach. It requires an awareness of proper software design, but it isn’t built in by default. There is nothing wrong with this, as long as developers understand the tradeoffs. But for the most part, they either haven’t understood them or have chosen to ignore them for the lure of short-term development speed over long-term maintainability.
To be clear, I’m not picking on any of these frameworks or the decision for a monolithic architecture. There is a time and place for both. In fact, I agree with David Heinemeier Hansson (DDH):
“Run a small team, not a tech behemoth? Embrace the monolith and make it majestic. You Deserve It!”
However, I would encourage teams to be thoughtful about the micro-decisions that they make as their application grows. These small decisions can add up to one big monolithic regret over time.
So, how can we avoid monolithic regret? By applying systems design to the development process to help us identify our module boundaries.
Designing a modular monolith
No matter the architectural style you choose, my recommendation for every product company and enterprise I advise is to focus on modularizing their applications. By breaking applications into modules, teams can better minimize the low cohesion that results in fragile codebases and slower development. It will also begin the preparation for a move toward a microservice architecture as your organization grows.
To understand how to modularize our monolith, we must apply systems design techniques. For those not familiar, or perhaps a little rusty, here is a quick review the core concepts of systems design:
Systems design is the process we use to identify and capture an application’s structure, behavior, and infrastructure. It involves decomposing the application into:
Systems: This is a completely independent system that provides a solution to one or more problems
Subsystems: Bounded concerns that help to compose the system itself. Subsystems do not offer a solution on their own, but contribute a bounded portion of it.
Modules: These are the building blocks for composing subsystems. Modules may contain one or more components, where components are classes in object-oriented programming languages or functions in functional programming languages.
Notice that you can also have subsystems that are decomposed into other subsystems. This is often the case in more complex solutions. Through the application of system design, we can determine the boundaries of our system and how we want those boundaries to interact.
How to find subsystem boundaries
One of the more difficult steps in applying systems design to software is finding the subsystem boundaries. If the boundaries are drawn incorrectly, it can lead to tight coupling between each subsystem and its modules.
My preferred approach is to first model the workflow that the application must support. This involves breaking down the requirements into the activities and steps necessary to accomplish the goals or tasks at hand. Those that have read the API design book I wrote with Keith Casey will recognize this approach.
By identifying these activities and steps performed by each participant, also known as an actor, we start to see a picture of how these steps are related:
Once the activities and steps are visualized, you can then start to find the boundaries where different participants are involved. Areas where only one particular party performs related steps form the subsystem boundaries:
Each subsystem should then have a well-defined, well-designed API to allow the application to perform the associated activities:
Whether you plan to implement your API within a single codebase as a monolith, or eventually decompose your APIs into microservices, this technique will help your subsystems and modules to be highly cohesive internally and loosely coupled between each other.
A word of caution about module dependencies and the DRY principle
If you plan on migrating to microservices, then the DRY principle must be used with caution. Dependencies on other modules will make it difficult to later migrate to microservices. Strive to be DRY within your modules and be willing to duplicate behavior across modules for easier migration to microservices in the future.
Moving toward microservices
Choosing to take a monolithic approach to your application architecture doesn’t have to end up in monolithic regret. By applying the proper design work as new features are implemented, your team can better modularize the application codebase. The result will be a more maintainable codebase and a team prepared for migrating to microservices in the future.
Want to learn more? Check out the slide deck below from my talk at the 2015 API Strategy and Practice Conference on designing long-lasting web APIs, where I covered these topics in more detail: