If you have a large and scary Monolith and looking for a clear path of migration to modern Microservices, this post is exactly about that. We are going to take a Monolithic application and together, step by step, walk through migration process. Importantly, our application is going to be live in production at all times while we execute our plan. Without further delay, let’s get started!
The First thing
Before we plan migration we need to answer a simple question why? Why do we need to migrate? What goals and business objectives are we going to achieve with migration? How our business can benefit? In general, Microservice architecture has multiple advantages compared to Monolith: faster features delivery (shorter time to Production), maintainability (smaller independent chunks of code are easier to work with), testability (it’s easier to automate testing for small independent code base), resiliency (if a single microservice has a bug, the system as a whole may still survive) and so on. If this what you need to achieve your goals and business objectives, great!
Microservice architecture does not come for free though. It takes a toll in many ways and for many things. For example, it’s is uncertain how to reuse code and to which extend. Should we copy-paste or create shared libraries? With Monolith it is rather simple, as there is one code-base where everything is shared. Microservices require certain infrastructure in place, for example a service discovery mechanism is a must. There might be hundreds or thousands of Microservices that somehow need to communicate. How do we know where to find the one we need, for example, to get a customer’s name by ID? With Monolith it’s as simple as calling a known function. Migration to Microservice architecture will require teams and even organisation as a whole to change adopting new techniques and practices. Handling thousand of independently deployable Microservices can not be done with same tools and processes that are used to handle a Monolith. Now we think about CI/CD and DevOps. If benefits of Microservice architecture overweight challenges that come with it, we are ready to get started!
Know what you have
Before we take out a sharp knife and start cutting the Monolith we need to identify components, that will clearly show us where we may or may not cut. By Component we mean a logically cohesive block of code that is responsible for implementing a single business domain concept. We may have physical modules (namespaces, libraries, packages and etc.) that do not correspond to logical components. If so, the migration process may be harder as we need to refactor physical modules in a way that they are aligned with logical components. For example, we have three-tier application with Presentation, Application and Data layers. A single logical component can be scattered across all three layers or modules. We need to bring business logic together into a single component that corresponds to business domain concept. It is recommended to keep User Interface as separate layer, but extract business logic and place it into a corresponding component.
Flatten and refactor logical components
It’s quite difficult to figure out a way to split a Monolith with a tall component’s structure that looks like Manhattan skyline. Here is an example of such a Monolith. The taller a “building” is the more deeply components are nested.
How do we even approach it? Well, we can start working from the top down and flatten tops of the “skyscrapers”. For the sake of simplicity, let’s just focus on
Below is how it looks after we zoomed in. We have two immediate sub-components:
marketing with their own sub-components. There is also common code ,orange colored boxes, that are shared among travel and marketing sub-components.
Let’s start with
travel component first, it seems to be the simplest to tackle as it’s already pretty flat and does not have any shared code among the sub-components.
We can simply flatten travel sub-components into a flat structure defining
travel.reimbursements components. We do not need travel component itself, because it is just a container and does not have any code of its own. This is what we have now.
Let’s continue and do the same thing with leads component. This is more complicated because the sub-components are nested deeper than with travel component.
Good for us, we can factor out
leads.print component right away. Now what do we do with the rest? It very depends on
leads.online code. One of possibilities is to factor out
leads.email into an independent component and then deal with the rest of
leads.online code. In our example we could factor it out into
leads.socialmedia as the code was all about social-media leads. This is how our structure looks like now.
Let’s move to the next component. This time we have common (shared) code among multiple sub-components.
We can factor out
messaging.email components using same approach as above. What do we do with common code? Let’s look closer to see if we can reduce the need for shared code, move it to appropriate modules or it might turn out that shared code implements a domain concept on it’s own and we can create a meaningful component out of it. However, let’s assume worst case scenario, when we still have some common code left and we can’t find home for it in any other component. At this point we can just create
messaging.common component like shown below.
Looks much better now, but we are not done yet. We still need to deal with marketing component.
By example above, we can go after immediate sub-components of marketing component. Common code may also be split and moved into the other components or may have a component of it’s own, same way as we did earlier for shared code of messaging component.
The structure is very flat now, but we have a problem: multiple components that are just shared code. What do we do about it? We need to become more creative now and refactor it!
We need to look close at the code and try to do the following:
- Find hidden domain concepts withing common code and create dedicated components for each.
- Find code that belongs to existing components and can be split and moved appropriately.
- Verify whether the code in fact needs to be shared among multiple components. Might turn out that only a single component uses it, therefore it belongs to it.
- If there is still any code left then we can create dedicated utility, common or shared component(s) for it.
Looking at the big picture we can see that sales component has very flat structure now. But the rest of Monolith is still pretty nested.
We need to continue further and work through the rest of Monolith, flatten and refactor until we get very flat structure.
Migrate to Macroservices
Now we are ready to start breaking down the monolith into Macroservices. Macroservices?.. Aren’t we migrating to Microservices? Well, yes, but the first step to Microservices is to have services. Leaping from Monolith to Microservices at once is quite complicated, especially considering that we are working with a live application that is running in production.
Once we have all components flat, we can draw domain boundaries and identify dependencies between domains. In other words, we group components based on their business domain role, then we identify how the groups depend on each other. The goal now is to have coarse-grained groups. Speaking in DDD terms, a component group may correspond to a sub-domain, bounded context or even a group of aggregates. If we go with more fine-grained groups we risk to be overwhelmed by the amount of services and exponentially growing dependencies between them. We haven’t done anything yet to prepare team(s) and infrastructure to support large amount of independently deployed services. For example, we have five groups of components defined for our monolith: travel, marketing, supplychain, warehouse and accounting. All groups are cohesive, coarse-grained and roughly of the same size. We have identified dependencies between all the groups as shown in the picture below. Good news, we do not have cyclical dependencies between component groups, otherwise we will not be able to migrate gradually, one service at a time.
Hopefully you can guess what becomes our Macroservices now. And you are right, each group of components we identified becomes a separate Macroservice!
Create remote User Interface
If a monolith has User Interface that runs on the same machine as the rest of the code and completely relies on that fact, we have truly monolithic application :). However Microservices assume remote communications, therefore we need to prepare User Interface to communicate “over the wires”. We can achieve that by creating a Proxy layer between UI and the rest of the application. Proxy layer helps to gradually transition our UI to use services keeping the application live at all time. Gradual transition mitigates large risks that come with “big-bang” releases. Remember to deploy often, do not wait until the whole monolith is fully migrated to services. If anything goes wrong, it is much easier to revert and fix a small piece rather than entire system.
- Create a Proxy layer that directly communicates to in-process domain code.
- When factoring out services, the Proxy can be gradually switching to use services instead of in-process code.
Proxy layer runs in the same process as UI and is responsible for communicating with the services.
Gradually Migrate to Macroservices
Once we have UI Proxy we can start decomposing the Monolith. First we can select a component group that does not have any outbound dependencies. In our case it is
accounting. Proxy now redirects all calls to accounting service. Other component groups, that depend on accounting, also redirect their calls to the service.
Next component group to decompose is
warehouse. It does not have any outbound dependencies on the rest of monolith. It only depends on
accounting service, this is exactly what we are looking for. Building dependencies from a service back to the monolith is very undesirable thing to do.
supplychain component group. It does not have any dependencies on the monolith so we can migrate it to a service. This is where we are now.
Moving along and migrating
travel component group into a service.
We don’t have much of code left in the monolith, so let’s go ahead and take the last
marketing service out.
At this moment we decomposed the monolith into Macroservices. We can look at our new service based architecture and begin thinking about migrating to Microservices.
First, we looked at our Monolithic application and made conscious decision we need to migrate to Microbreweries. We did consider pros and cons and evaluated risks. Next, we refactored and grouped the code into logical components to make migration possible. We identified cohesive component groups and mapped dependencies between them. Then we gradually migrated the Monolith to Macroservices, one service at a time. Now we are going to take it further and migrate service based architecture to Microservices, but that is the whole new post.
|Monolith to Microservices: Evolutionary Patterns to Transform Your Monolith, by Sam Newman.|
|Microservices Patterns: With examples in Java, by Chris Richardson.|