According to a 2020 O’Reilly report on microservices adoption, 77% of businesses have already adopted microservices, while 92% of them state they are experiencing success after migrating monolith to microservices. The trend to en-masse start using microservices application architecture comes as no surprise considering the multiple benefits that the microservices architecture provides, including:
- Improved resilience
- Increased scalability
- Continuous delivery
- Shortened Time to Market
- Improved ROI with reduced TCO
- Streamlined maintenance and easier debugging
- Increased ability to use the right tool for the right task
Monolithic vs Microservices
A typical monolithic application involves an architecture where all actions and data objects are underpinned by a single and close-knit codebase, with data being kept within a single database. This storage mechanism is used to access the data via developed functions and methods, while the business logic component is located on the server codebase and the client application. The monolithic architecture involves a system that functions as a single, autonomous, and indivisible unit. Modifying and extending such a system is a relatively complex endeavor as a small change within a certain component of the system can sometimes severely impact other components due to the tight interconnectedness of the whole system.
Microservices-based apps, however, involve business logic that divides the components into lightweight, single-purpose stand-alone units. This architecture implies that each microservice is a self-sufficient component encompassing its own code and data, with no dependencies on other components. Deployment is done within separate software containers that are managed by container orchestrators.
The monolithic app can be transformed and moved onto microservices architecture in such a way that its own data storage mechanisms, UI, and data schema become a unified microservice set. Each microservice in this set performs the same functions as the original applications under a single UI.
Migrating an app from monolith to microservices provides the following set of tangible advantages:
- Mitigating manual entry duplication
- Minimizing programmatic development risks
- Allowing for a single, unified view of the data
- Improved system control and synchronization
A Step-by-step Guide for Monolith to Microservices App Migration
Before we move onto actual steps, let’s get a quick overview of technical aspects involved in microservices migration strategies:
Independent Packaging – Be sure to separate the related modules into independent packages rather than coupling them all in a single package. The process is likely to require minor code or static content alterations if you choose to change the context roots of the application so they are separate for each module package.
Container per Service – The app refactoring is best done by using the container per service strategy and assigning a separate server to each package, ideally each of them in its own container (like Docker containers, for example). This way you’ll be able to scale each container independently.
DevOps – When the modules are separated, an automated DevOps pipeline (like CI/CD) enables you to manage each package independently. This enables you to leverage all the advantages of the continuous delivery process, the most beneficial of which is being able to build, deploy and manage independently.
That said, we’re moving on to the actual process.
Step 1: Identify Microservice Candidates & Create a Backlog*
Prior to performing any other monolith to microservices migration phase, it is highly recommended that you identify candidates and create a monolith to microservices process backlog. The candidates include functionality components that will be transformed into separate microservices. Your first step is to prioritize these functionality parts in order to create a logical queue for subsequent microservices migration steps.
These candidates should typically be isolation-friendly functional groups, which means they need to be capable of being easily separated from the monolith and thus resolve certain issues the app has. For example, good candidates are parts that have performance- and/or resource-based problems, as well as domain areas capable of unblocking other microservices’ separation from the monolith – like a functionality that is being widely used by other monolith parts.
Once you have a firm grasp of which components you will be extracting, in which order and why – you should select the first candidate in your queue and decide which app refactoring strategy you will deploy.
Step 2: Choosing the Right App Refactoring Strategy
There are two ways you can migrate the selected functionality from monolith to a separate microservice:
Monolith to Microservices Strategy 1: Domain-Driven Design
The Domain-Driven Design tactic involves isolating a chosen functionality within the monolithic architecture and subsequently separating it into a microservice. This software development approach requires an understanding of the domain for which the application will be written and implies an incremental removal of connections with other functionality. The candidate is being kept within the monolithic architecture up until the point when all the connections are gone and the new API is ready. Only then can the candidate be separated from the monolithic structure and be rolled out as a standalone microservice.
Monolith to Microservices Strategy 2: Strangler Pattern
The Strangler Pattern strategy involves creating an independent copy of a functionality that is then further developed and turned into a microservice, while the original functionality is kept operational within the monolith. Once the new copy-turned-microservice is successfully implemented and tested along with its integrations, the original monolith-based functionality is decommissioned.
Both tactics are quite efficient and share the same vulnerability – new dependencies may ensue between the monolithic architecture and the candidate functionality once the monolith to microservices migration process is complete. These newly emerged dependencies should be thoroughly examined and resolved via shared libraries with common code components or through new API endpoints.
We will focus on the Domain-Driven Design strategy as this approach is better for developing apps that have complex business requirements that tend to change relatively frequently, which is why this strategy is needed by modern companies.
Step 3: Stop the Development of the Monolithic Application
You should not add more code to the monolith during the process of new functionality implementation. The new code is to be added to the new standalone microservice rather than the monolith, which involves installing a request router/API Gateway. This gateway deals with incoming HTTP requests, routing them either to the new microservice or the existing monolith.
As service needs to access monolith-based data on a frequent basis, you should choose how it will do that. Typical monolith data access strategies include:
- Using a remote API provided by the monolith
- Directly accessing the monolith-based data
- Using data copies synchronized with the monolith’s database
Step 4: Frontend/Backend Separation
During this phase, the monolithic application is shrunk and the presentation layer is split between the business logic and data access layers. Complex apps typically involve the following 3 component types:
The Presentation Tier – managing HTTP requests and the implementation of either an HTML-based web UI or the (REST) API. Apps that feature sophisticated UIs typically have a presentation layer in the form of hefty code.
Business Logic Tier – involves components that represent the app’s core, implementing the business rules. Data-access Tier – involves a layer of components that access infrastructure parts (like databases, message brokers, etc).
Step 5: Set Up/Change CI/CD
It is recommended that you start altering your CI/CD pipeline at the same time you start the microservice development process as it will help you successfully implement and launch the new microservices. Continuous Integration helps you easily streamline code integration into a shared repository, while Continuous Delivery enables you to continually deliver this repository code to production.
Some dev teams also use Continuous Integration to turn the repository code into a deployment-ready compiled package and store it in Artifactory. In this context, Continuous Delivery helps you deploy these Artifactory-based compiled packages to the destination architecture, which means that CI and CD processes do not intersect and are not mutually coupled in a tight manner so you can improve them independently.
Step 6: Extraction/Implementation
During this stage, the existing monolith modules are turned into standalone microservices, the tightly-coupled connections with the parent app are severed, and the API gateway is implemented. This gateway is now a single communication point between the microservices and monolithic app, and/or other microservices.
The monolith-based app gets smaller with every module that is extracted and transformed into a microservice, and once the sufficient number of modules gets converted, the monolith is eliminated completely.
Step 7: Testing
Deep analysis and testing must not be neglected during the process of microservices implementation. The testing phase enables you to ensure all potential issues are mitigated and tackled before the new microservices-based infrastructure is rolled out.
We recommend that the testing phase overlaps with the microservice implementation process, while another best practice is starting it even earlier – during tools selection and planning. This way, you will be able to monitor the monolith to microservices migration process more accurately and as early as possible, allowing you to check if the new environment would work exactly the way you wanted.
Once the microservices are operating successfully within the new architecture, the testing may stop and the end result of the implementation step is a fully functional and independent microservice capable of operating on its own, isolated from the monolithic app.
*Note: SuperAdmins specializes in the implementation of cloud-native solutions and consulting in the integration of DevOps technologies and processes in customer’s SDLC. We do not provide developer services to which steps 1 and 4 from this guide belong. However, we can provide those services through our partner companies which specialize in application development – so please do not hesitate to contact us for assistance with your monolith to microservices migration.
The Role of DevOps in Monolithic to Microservices Migration
DevOps is among the critical parts of this process. When creating a microservices-based architecture, DevOps takes the role of speeding up the process of delivering high-quality apps by enabling the dev team and the IT operations team to work closely together. The DevOps pipeline helps the dev team get a deeper insight into the production environment in which their app will run, allowing them to improve the quality of software. In fact, the whole microservices-based architecture stems from the DevOps mindset and ideologies that emerged from giant brands like Google, Amazon, Netflix, Facebook, etc.
Some of the business benefits that deploying DevOps can provide include:
- Improved app releases frequency
- Improved code and product quality
- Reusability of developed software
- Increased automation, productivity and innovation
- Minimized human error risk
- Big reductions in app development costs
- Improved business value
The main idea behind the DevOps approach to creating microservices is to diffuse software programs into smaller modules in order to enable higher product quality and faster software delivery. This is achieved through the following 4 main DevOps practices:
- Continuous Integration
- Continuous Deployment
- Continuous Delivery
- Continuous Testing
Within the microservices environment, DevOps teams are able to produce standalone functionality pieces in a parallel and streamlined manner, rather than having to move code from the development phase to testing stages and production. Instead, these cross-functional DevOps teams are capable of building, testing, releasing, monitoring and maintaining the app in a cooperative fashion.
Here are quick and actionable tips for using DevOps with microservices:
- Keep the services independent to enable easier testing of separate components.
- Have a security-first approach to DevOps by using updated security tools and incorporating the process of continuous monitoring of potential security risks.
- Keep the single-repository-per-service strategy to prevent code cross-population across different services.
- If possible, use the same building blocks with similar endpoints on which you will create a CI/CD pipeline for each service.
- Use semantic versioning to build and deploy.
- Since each service has its own database, be sure that multiple database deployments are supported within test/staging environments.
- Use container technologies to enable a more agile and scalable microservice architecture.
- Don’t forget to follow compliance and data governance best practices.
Migrating your app from monolith to microservices can be quite a convoluted process. However, this complex task can be completed successfully and in a timely manner if done the right way. Each app has a somewhat unique architecture and not all workflows, processes and operations are the same, which means that your monolith to microservices app migration strategy should be tailored according to your own needs and goals.
This entire endeavour, though difficult to execute properly, can help your business leverage all long-term benefits that microservices-based infrastructure can provide, with a focus on scalability and agility. It does require a multifaceted approach and highly skilled specialists, but the payoff is manifold and definitely worth it.