Migrating Monolith to Microservice Architecture – the Path

If you have a large and scary Monolith and are 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 the 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?

  • Why do we need to migrate?
  • What goals and business objectives are we going to achieve with migration?
  • How can our business benefit?

In general, Microservice architecture has multiple advantages compared to Monolith: faster feature delivery (shorter time to production), maintainability (smaller, independent chunks of code are easier to work with), testability (it’s easier to automate testing for a 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 is 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 is uncertain how to reuse code and to what extent. 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 organisations as a whole to change, adopting new techniques and practices. Handling thousands of independently deployable Microservices cannot be done with the same tools and processes that are used to handle a Monolith. Now we think about CI/CD and DevOps. If the benefits of Microservice architecture outweigh the challenges that come with it for you, 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, 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 a 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 the business domain concept. It is recommended to keep User Interface as a separate layer but extract business logic and place it into a corresponding component.

A single logical component ShoppingCart is spread across three layers.
A single logical component ShoppingCart is spread across three layers.

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 the Manhattan skyline. Here is an example of such a Monolith. The taller a “building” is, the more deeply components are nested.

Monolith with deeply nested components.
Module’s structure of the monolith.

How do we even approach it? Well, we can start working from the top down and flatten the tops of the “skyscrapers”. For the sake of simplicity, let’s just focus on the sales component.

Zoom into sales component.
“Sales” component selected.

Below is how it looks after we zoomed in. We have two immediate sub-components: travel and marketing, with their own sub-components. There is also common code , orange-colored boxes, that are shared among the travel and marketing sub-components.

Sales component zoomed-in.
“Sales” component zoomed in.

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.

Selecting travel component.

We can simply flatten travel sub-components into a flat structure defining travel.planning, travel.tickets, and travel.reimbursements components. We do not need the travel component itself, because it is just a container and does not have any code of its own. This is what we have now.

Travel component it flat.

Let’s continue and do the same thing with the leads component. This is more complicated because the sub-components are nested deeper than with the travel component.

Leads component before being flattened.

Good for us, we can extract leads.print component right away. Now what do we do with the rest? It very much depends on leads.online code. One possibility 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 now.

Leads component is flat now.

Let’s move to the next component. This time we have common (shared) code among multiple sub-components.

Messaging component with common (shared) code.

We can factor out messaging.text and messaging.email components using the same approach as above. What do we do with the 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 its own, and we can create a meaningful component out of it. However, let’s assume the worst-case scenario, when we still have some common code left and we can’t find a home for it in any other component. At this point, we can just create a messaging.common like shown below.

Messaging component is flat now.

Looks much better now, but we are not done yet. We still need to deal with the marketing component.

Marketing component to be flattened.

By example above, we can go after immediate sub-components of the marketing component. Common code may also be split and moved into the other components or may have a component of its own, the same way as we did earlier for shared code of the messaging component.

Marketing component is flat now.

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!

Refactor common code.

We need to look closely at the code and try to do the following:

  • Find hidden domain concepts within 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. It 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 a dedicated utility, common, or shared component(s) for it.
Sales sub-components are flat now.
Sales sub-components are all flat now.

Looking at the big picture, we can see that the sales component has a very flat structure now. But the rest of Monolith is still pretty nested.

Monolith structure after flattening sales component.

We need to continue further and work through the rest of Monolith, flatten, and refactor until we get a very flat structure.

Continue flatten and refactor.

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.

Draw boundaries

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 being 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 a large amount of independently deployed services.

For example, we have five groups of components defined for our monolith: travel, marketing, supply chain, 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.

Boundaries and dependencies.
Mapping monolith to Macroservices.

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 a User Interface that runs on the same machine as the rest of the code and completely relies on that fact, we have a truly monolithic application 🙂. However, Microservices assume remote communications; therefore, we need to prepare the User Interface to communicate “over the wires”. We can achieve that by creating a Proxy layer between the UI and the rest of the application. The The Proxy layer helps to gradually transition our UI to use services, keeping the application live at all times. 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 the entire system.

  1. Create a Proxy layer that directly communicates with in-process domain code.
  2. When factoring out services, the proxy can be gradually switching to use services instead of in-process code.
User Interface and Proxy

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. The proxy now redirects all calls to the accounting service. Other component groups that depend on accounting also redirect their calls to the service.

Decompose to accounting service.

Next component group to decompose is the warehouse. It does not have any outbound dependencies on the rest of the monolith. It only depends on the accounting service; this is exactly what we are looking for. Building dependencies from a service back to the monolith is a very undesirable thing to do.

Decompose to warehouse service.

Next is the 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.

Decompose to supplychain service.

Moving along and migrating the travel component group into a service.

Decompose to travel service.

We don’t have much of a code left in the monolith, so let’s go ahead and take the last marketing service out.

Monolith decomposed to macroservices.

At this moment, we decomposed the monolith into microservices. We can look at our new service-based architecture and begin thinking about migrating to Microservices.

Summary

First, we looked at our Monolithic application and made a conscious decision that we need to migrate to Microbreweries. We did consider the pros and cons and evaluated the 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 a service-based architecture to Microservices, but that is the whole new post.

Further Reading

Monolith to Microservices: Evolutionary Patterns to Transform Your Monolith, by Sam Newman.
Microservices Patterns: With examples in Java, by Chris Richardson.

Posts created 30

One thought on “Migrating Monolith to Microservice Architecture – the Path

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top