Transition from a monolith to microservice architecture
technical articlesFebruary 15, 2023

Transition from a monolith to microservice architecture

An overview of microservices and best practices

Article presentation
The transition from a monolith to microservice architecture, best practices, decomposition strategies, and refactoring using a strangler application.

“The future is already here - it’s just not very evenly distributed” William Gibson, science fiction author

I wanted to begin with this quote as it suggests that it takes time for ideas and technology to be assimilated by the community and even longer to become widely accepted. That was also the case with microservices. In the early days, big companies such as Amazon, Cloud Foundry, and eBay initially used a monolithic architecture that had a combined diversity of functions resulting in development challenges. You could say that these applications rapidly exceeded the limits of their architecture. As a result, they started gradually transitioning from a monolithic approach to a new architecture of loosely coupled services. Although this was the experience for them, as we know, this is not a universal solution. It simply means there is still an ongoing discussion regarding the best approach to follow when deciding between monolithic architecture and microservices.

This blog post aims to bring to light some crucial considerations you should be aware of when making the decision to transition from a monolithic to a microservice architecture. Our objectives are as follows: 

  • First, we will provide an overview of the issues that can arise when a monolithic architecture outgrows its design and becomes a "big ball of mud";
  • Next, we will examine how a microservice architecture can offer a solution and explore why applications should be broken down into different modules that can be developed and understood by different teams;
  • We will delve into two key decomposition strategies;
  • And wrap up by discussing some important points to keep in mind when refactoring a "big ball of mud" into a microservice architecture.


Symptoms of monolithic hell

For the purpose of this blog post, we will explore a real-world example: FTGO, one of the top online food delivery companies in the US.

At its core, FTGO is a straightforward platform. Customers use the FTGO website or mobile app to place food orders at local restaurants and FTGO then manages a network of couriers to deliver the orders and handle the payment for both couriers and restaurants. Restaurants, in turn, use the website to manage their menus and orders.

FTGO monolith architecture


As depicted, the initial FTGO architecture is hexagonal, with the business logic forming the core and various adapters surrounding it to implement the user interface, REST API, and integrations with external services such as MySQL.

In its early stages, a monolithic architecture is not necessarily a drawback. It is often easier to develop, as you are focused on a single application, and changes to the code and database schema require only a rebuild and deployment. Testing and deployment are straightforward, and scaling is simple as you only need multiple instances of the single application behind a load balancer. Additionally, smaller team sizes can also be a factor.

Unfortunately, monolithic applications have several significant drawbacks. As the code base and development team size grows, the once simple application becomes more complex and large, making it difficult for any one developer to fully understand. Development slows down and building the application takes longer, negatively affecting productivity. With many developers contributing to the same code base, the build is often in an unreleasable state, and even with solutions like branching, merging can still become difficult and time-consuming. Deploying changes to production is also a slow and potentially challenging process, with perhaps one or two deployments happening during off-hours. Additionally, running the entire test suite can take a significant amount of time, sometimes even requiring manual intervention from developers.


FTGO monolith

In addition, there are other factors that pose challenges to a monolithic architecture. The architecture may struggle with conflicting resource requirements among its modules. These modules don’t require memory but CPU and modules that require memory but less CPU are part of the same application, resulting in the need for compromise on server configurations. The lack of fault isolation is also a concern, as all modules run within the same process and an obsolete technology stack may be a hindrance to future growth and advancements. The cost and risk of rewriting the entire monolithic application with new and better technology can be significant, leaving developers stuck with the initial technology choices made at the start of the project and potentially facing compatibility issues with newer versions of frameworks.

The symptoms of monolithic hell:

  • Increasing code base and code complexity
  • Multiple Scrum teams working on particular functional areas
  • Development is slow and painful
  • Scaling becomes difficult
  • Lack of reliability and fault isolation
  • Forced to use technology stack becoming increasingly obsolete


Microservice architecture to the rescue

Architecture plays a critical role in the success of an application. Despite having a disciplined team and best practices, working on a monolithic application can still result in the drawbacks mentioned earlier. Currently, the trend is to adopt microservice architecture for large and complex applications.

Microservice architecture is an architectural style that divides an application into smaller, functional services. It's important to note that software architecture should meet not only functional requirements, but also consider non-functional requirements such as maintainability, extensibility, testability, and scalability.

How to do that?! In his book “The art of Scalability”, Michael Fisher describes a three-dimensional scalability model which defines three separate ways to scale an application (the scale cube).


Microservice architecture - the scale cube

While the X and Z-axis scaling improve the application’s capacity and availability neither approach solves the problem of increasing development and application complexity. To solve those, you need to apply the Y-axis scaling or functional decomposition by splitting a monolithic application into a set of services developed and understood by different people.


Microservice architecture - Y-axis scaling

Let’s examine how the microservice architecture is a better fit for the FTGO application and also expose some benefits and drawbacks to it.


Microservice architecture to the rescue

Benefits of microservice architecture:

  • Enables continuous delivery - reduces time to market, which enables businesses to rapidly react to feedback from customers. Also, employee satisfaction is (...or should be) higher as more time is spent delivering features instead of addressing fire-fighting situations.
  • Services are (relatively) small and easily maintained - code is easier to understand, and the code base doesn’t slow down building and start-up as we’ve seen in the monolithic architecture thus developers are more productive.
  • Services are independently scalable and deployable - which is again quite different from the monolithic architecture where components CPU intensive vs memory intensive must be deployed together.
  • Enables autonomous teams - you can structure an engineering organization as a collection of small autonomous teams.
  • Has better fault isolation - the microservice architecture has better fault isolation. For example, a memory leak in one service only affects that service
  • It allows experimenting and adoption of new technologies, eliminating any long-term commitment to a technology stack. In principle, when developing a new service, the developers are free to pick whatever language and frameworks are best suited for that service.

Drawbacks of microservice architecture:

  • Finding the right set of services is challenging.

One challenge with using it is that there isn’t a concrete way to decompose a complex system into services. To make matters worse, if you do it incorrectly, you might end up with a distributed monolith with coupled services that must be deployed together. In Romanian, there is actually an expression for this - ‘struto-camila’ - which is the combination of an ostrich and a camel.

  • Distributed systems are complex, which makes development, testing, and deployment difficult.

Building a microservice architecture also presents developers with the challenge of managing the complexity of creating loosely coupled services that communicate with each other. This requires the use of an interprocess communication mechanism and the ability to handle partial failures, which can be more complex than simply calling a method. In certain cases, where each service has its own database, transactions that span multiple services must be implemented to maintain data consistency. This can be achieved through the use of the saga pattern or by implementing API composition or CQRS views to handle complex queries.

  • Deploying features that span multiple services requires careful coordination.

You have to create a rollout plan that deploys services based on dependencies between them.

  • Deciding when to adopt the microservice architecture is difficult.

And finally, I want to reiterate that is difficult to decide whether the microservice architecture is the right choice for you or not. There are multiple factors that need to be taken into account and you should do the due diligence.


Decomposition strategies

Let's examine how we can decompose an application into services. Keep in mind that services should be organized around business concerns, rather than technical concerns. Previously, we saw that FTGO started with a hexagonal structure, which was a good choice at the time, but as the business evolved, there was a need to migrate to microservices. This can be accomplished by using decomposition strategies, and we'll briefly explore decomposition by business capability and decomposition by subdomain.

   A. By business capability

One strategy for creating a microservice architecture is to decompose by business capability. A business capability refers to an activity that a business performs to generate value. These capabilities tend to be stable, unlike the way an organization operates, which can change over time. Business capabilities are often centered around a specific business object. For example, in the case of FTGO, the business capabilities include order management, courier management, and restaurant management.

Identifying application boundaries makes it relatively easy to map other capabilities. You then define a service for each capability or a group of related capabilities. Eventually, you will have something similar to the image below.


Decomposition strategies - business capability

This would be a first attempt at defining the architecture because, in time, services may evolve as you learn more about the application domain.

   B. Decompose by subdomain

The second strategy for creating a microservice architecture is to decompose by sub-domain, which involves defining services that correspond to the subdomains in Domain Driven Design (DDD). DDD is a different approach to enterprise modeling compared to the traditional approach, which creates a single model for the entire enterprise, such as customer, order, etc. The challenge with this kind of modeling is that it can be difficult to get different parts of an organization to agree on a single model.

DDD addresses this issue by defining multiple domain models, each with a clear scope. A separate domain model is defined for each subdomain, which is a part of the overall domain. Subdomains are identified using the same method as identifying business capabilities: by analyzing the business and identifying distinct areas.

DDD refers to the scope of a domain model as a "bounded context." When using a microservice architecture, each bounded context corresponds to a service or a set of services. Therefore, by applying DDD, we can create a microservice architecture by defining a service for each subdomain. 

The end result is very likely to be subdomains that are similar to the business capabilities.


Decomposition strategies - Decompose by subdomain

Decomposing by business capability and by subdomain are two main strategies for defining an application’s microservice architecture. 

There are however some useful guidelines from object-oriented design principles such as:

  • SRP - single responsibility principle, 
  • CCP - common closure principle.

And of course, along the way, you will face some obstacles such as:

  • Network latency;
  • Reduced availability due to synchronous communication;  
  • You would have to maintain data consistency across services;
  • Obtain a consistent view of the data;
  • God classes preventing decomposition.


Refactoring the monolith to a microservice architecture

In summary, we've discussed the advantages and disadvantages of monolithic architecture and explored how microservice architecture can address the challenges of monolithic architecture. We've also covered some strategies for decomposing an application into microservices. But, the question remains, how can you migrate to a microservice architecture without starting from scratch?

Transforming a monolith into microservices is a complex task that requires time, resources, and a pause in implementing new features. Most businesses will support this migration if it addresses a significant business problem, such as slow delivery and poor scalability.

One approach to consider is to incrementally convert the monolith into microservices by developing what's known as a "strangler application". This method will be discussed further.

Refactoring to microservices - Strangler application pattern

Transforming a monolithic application into microservices is a form of application modernization that converts legacy code into a modern architecture and technology stack. On the other hand, a big bang rewrite, where you completely redo the microservice architecture from scratch, may sound tempting, but it's highly risky and time-consuming. It could take months or even years to replicate existing functionality before you can even begin to add new business features.

A better approach is to incrementally refactor the monolith into microservices, by building a "strangler application" that runs alongside the monolith. This method allows you to gradually replace parts of the monolith with microservices, reducing the risk and increasing the chances of success in your application modernization journey.


Refactoring to microservices - Strangler application pattern

Over time the amount of functionality implemented by the monolith shrinks until either it disappears entirely or it becomes another microservice. 

It’s important to note that a service is rarely standalone, it needs to collaborate with the monolith. Sometimes a service needs to access data owned by the monolith or invoke its operations. The monolith might also need to access data owned by a service or invoke its operations. 

To facilitate this communication, you'll need to design an integration mechanism between the monolith and services. This "glue" can take the form of a REST client in the service, and web controllers in the monolith, if the service invokes the monolith using REST. If the monolith subscribes to domain events published by the service, the integration glue will consist of an event-publishing adapter in the service and event handlers in the monolith.

Finally, it's important to avoid making extensive changes to the monolith during the migration to a microservice architecture. This can help minimize disruption and reduce the risk of encountering unexpected issues during the transition.


Conclusions

We have reached the conclusion part of this article and here are some recommendations for a smooth transition from a monolith to a microservice architecture:

  • The technical team must analyze the business model before adopting microservices or a monolithic structure.
  • Before migrating to a microservice architecture, it’s critical to identify if the existing problems are, in fact, a result of having outgrown the monolithic architecture.
  • Migrating to microservices is all about taking incremental steps, and one way you can achieve that is by using the strangler application.

 a. A great way to introduce microservices into your architecture is by implementing new features as services.

 b. Break up a monolith by incrementally migrating functionality from the monolith into services.

I will recommend reading 2 important books in this field, books that inspired me and continue to provide valuable information on the subject:

  • The art of Scalability, Michael Fisher
  • Microservices patterns, Chris Richardson


Copywrite images: Chris Richardson in book Microserviesc patterns.