Transition from a monolith to microservice architecture
An overview of microservices and best practices
An overview of microservices and best practices
“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:
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.
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.
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:
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).
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.
Let’s examine how the microservice architecture is a better fit for the FTGO application and also expose some benefits and drawbacks to it.
Benefits of microservice architecture:
Drawbacks of microservice architecture:
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.
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.
You have to create a rollout plan that deploys services based on dependencies between them.
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.
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.
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.
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:
And of course, along the way, you will face some obstacles such as:
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.
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.
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:
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:
Copywrite images: Chris Richardson in book Microserviesc patterns.