Monoliths are the traditional way of building applications. A monolith starts as a small piece of software that grows over time with new features. Unfortunately, this all-in-one software approach makes it difficult to scale our applications. Even when we build a monolith as a well-organized structure, our application’s codebase can become so large that individual developers and teams don’t understand how it all fits together.
In contrast, microservices make scalability possible by breaking the application into independent yet integrated components and services. In this architecture, a system can increase or decrease performance and cost by adjusting the resources allocated to the components. With microservices, we can handle the number of instances allocated to each service as needed automatically. This is done using a container orchestration system, such as Kubernetes.
In a distributed system, multiple teams can independently develop different features in separate services. When successfully implemented, a microservices development culture enables each team to be autonomous, requiring minimal coordination to work with other teams. As a result, it boasts improved code quality and long-term code ownership.
This is all great if you build your application with microservices from the outset, but what about existing monolithic applications? Let’s look at some steps you can take to deconstruct a monolith gradually and replace it with a microservices application.
Modernizing an existing monolith isn't the same as completely replacing it by developing a new, equivalent application based on microservices. Developing a new application would introduce so many risks to the project that rollout would likely fail, causing stakeholders to push for a return to the original monolith.
Successful evolution from monolith to microservices requires building both alongside each other. But a development team should be wary of migrating old code right away. Migrating can be complicated and confusing, especially when the team lacks appropriate planning and guidance.
Instead, start by building new features as microservices. By starting small and building slowly, teams have time to establish expertise in distributed application development. In this period, developers can contribute ideas, make mistakes, and find efficient ways to do things. This time helps teams become prepared for challenging tasks in the future.
Examples of new features that would be suitable candidates to develop as microservices include:
If running in the cloud, consider building initial microservices using services such as AWS Lambda, Azure Functions, or Firebase Functions. These products implement function as a service (FaaS), a type of serverless computing that allows applications to run in a cloud computing execution model without a need for server provisioning and management.
Cloud functions can ease development efforts in transitioning to microservices. With FaaS and serverless computing, we can run backend code without managing our server systems or applications—giving us time to adapt to the microservices approach. Meanwhile, operations teams can gradually build out and scale a microservice-friendly architecture.
Distributed systems pose challenges that we don’t have to worry about in a monolith architecture. For example, different backend services communicate with each other across a network. So, networking and observability become critical areas where most operational problems occur.
Developing distributed services requires different tools from developing monoliths. Debugging different processes is not an option. Identifying root causes while transactions run across multiple services is often a complicated and time-consuming task.
This is where distributed tracing comes in. While traditional tracing can deal with requests within a single-process monolith, distributed tracing can track a single request through multiple processes.
We can use tracing and analysis tools like Jaeger, Prometheus, OpenTelemetry, and Grafana to identify the parts of our monolith that see the most use and traffic. These are ideal points to start splitting code out of our monolith to scale each service separately.
Once we split high-traffic and high-load parts of a monolith into microservices, we should split the rest of our monolith along functional lines. For example, this includes authentication, notification, and logging. To help with this task, let’s look at two beneficial microservices patterns: the strangler pattern and the branch by abstraction pattern.
The strangler pattern allows us to migrate from a monolithic application to microservices incrementally. It does this by extracting parts of a monolith functionality and introducing them in the new service.
Once we complete this process, the old monolith functionality is “strangled.” That is, we have made it obsolete and phased it out. We can sum this up in three steps:
As the strangler application grows and we replace more functionality with new services, the monolith shrinks over time.
As you can imagine, the strangler pattern is the recommended approach for functionalities already exposed as API endpoints by the monolith. For example, this includes REST and GraphQL APIs and SOAP web services. The benefit of this pattern comes from the fact that we can migrate functionalities without even touching the monolith’s code. But, the feasibility of the strangler pattern depends on the level of modularity presented by the monolith. It also depends on whether or not it exposes its functionalities with public endpoints.
When we can’t easily intercept calls and create a proxy to reroute the requests, we can benefit from the branch by abstraction technique. It’s like a strategy design pattern applied to a more extensive scope.
Branch by abstraction requires us to modify the existing monolith’s code. It works by implementing the next few steps:
Finally, once the new microservice abstraction of the functionality is stable, we must deprecate the old monolith’s functionality. Later, to avoid confusion, we’ll remove it entirely from our source code.
Migrating from a monolithic architecture to microservices offers many benefits. These include scalability and the ability to assign many developer teams to build different services simultaneously.
Modernizing a monolithic application is a multi-team development effort. It takes planning and deliberate work to break a monolith into smaller parts. Our tour described how to leverage tools, patterns, and methods tested in the field to guide our efforts towards a safe and consistent microservices development. So, when it’s time to say goodbye to your monolith, you have the knowledge you need to prepare for a successful migration.
Marcelo Ricardo de Oliveira is a senior freelance software developer who lives with his lovely wife Luciana and his little buddy and stepson Kauê in Guarulhos, Brazil. He is the co-founder of the Brazilian TV Guide TV Map and currently works for Alura Cursos Online.