In the previous post we broke down the monolith into coarse-grained Macroservices based on their business domain purpose, that changed our architecture style from Monolith to Service based. We refrained from migrating to Microservices right after and decided to wait a little before taking next round. Now, as our application is stable and running smoothly, we are ready to take it further. So, without further delay, let’s get started.
What is Microservice
Before we begin migration to Microservices, we need to clearly understand what Microservice is. What does it take to make a service micro? What are the characteristics of Microservices?
Micro is small. But how small, how many lines of code? This is not about lines of code, it’s about responsibilities. How many things the service is responsible for? A Microservices does one thing but it does it well. Speaking in Domain-Driven Design terms I would keep a microservice responsible for a scope as large as bounded context and as small as an aggregate.
Microservice is independent unit of development. Each microservice can be developed completely independently, as long as we have agreed upon API. For each microservice we have an option to select a programming language and a framework that is “best tool for the job”. We are not bound to a single choice for an entire system. One programming language may be a good fit for implementing one part of a system, but terrible choice for the other.
Microservice is independent unit of deployment. We can deploy a single microservice at any point in time without considering the rest of the system. Deployment of each microservice is independently reversible, if needed to.
Microservice is independent unit of scale. One parts of a system may have to process more requests than the other, or even encounter different load patterns based on time of the day, day of the week end etc. We can run more or less instances of a microservice based on expected or current load. That allows to fine tune the balance between performance and cost of infrastructure. The difference can be significant, especially if running on a cloud infrastructure, e.g. AWS.
Microservice functionality is available through API and API only. No other part of the system can access or directly manipulate the data microservice owns.
As we can see Microservices are quite autonomous creatures: have API as a single point of contact and source of creating dependencies, responsible of one thing, have ownership of data, can be implemented, deployed and scaled independently.
How to get ready for Microservices
We definitely need rollout and rollback strategies. We can put in place proxies to redirect clients of the APIs we migrate to old or new implementations, gradually or all at once. Anything we deploy we should be able to rollback immediately.
Various deployment strategies can help too, for example one of the techniques is blue green deployments. With this technique we have two parallel production environments, one is active and serves clients, and the other holds previous release. If we need to roll back, we just switch proxy to route clients back to previous release. To deploy new version, we push changes to inactive environment and then switch client’s requests to hit new version. Another approach is canary deployments. With this approach, instead of rolling new functionality to all users, we can verify a change in production environment by routing only a subset of users to new functionality. Once we sure that new functionality works, we switch the rest of the clients to use it.
As number of Microservices growth, the need for automation emerges. Managing deployment of hundreds or thousands of independent bits is very complex and error prone. Continuous Integration and Continuous Delivery (CI / CD) come to rescue. CI / CD can automatically check quality of code, verify behavior by running automated tests, package code and deploy a Microservice to production.
We also need service discoverability. When one Microservice needs to talk to the other it should have a way to find the address of the other service, e.g. look up IP address and Port by some alias name. Or maybe, we run our Microservices in Kubernetes, that among the rest will give us service discovery.
We definitely need observability, that includes logs, metrics and alarms. With large amount of services running, we need tools that provide us with coherent view of our system health. We need automated notifications if anything goes wrong and ability to trace a problem back to an instance of running Microservice.
Teams need to be comfortable with all the tools and techniques in order to work effectively with microservices, therefore it will take some time to transition to this mode of operation. Start migration slowly. We need to learn to walk before we can run. Think about choosing the part of the system that can teach you the most about the process, and have low impact if we make a mistake.
What is the first pick?
Now we are ready to start working on migration. If we take our Service based architecture as an example, where do we start first?
There are two orthogonal forces that impact your decision:
- One pulls toward the easiest to migrate and less impactful component.
- Second pull toward the component that will benefit the most from migration.
We map our Macroservices to the chart to arrange them based on simplicity and importance of migration. The best bet for us is to select important and simple, that corresponds to the upper-right corner.
Based on our chart there are two good candidates for migration: marketing and travel services. Travel service is a bit simpler to migrate, but users will get more benefits from marketing service migration. So, marketing service, let it happen!
The steps
Now let’s take marketing service and walk through migration steps together. While looking closely at the service, we can identify four main themes it’s responsible for: leads, messaging, tracking and campaigns.
These themes can be our four coarse grained services to start with. Even though they use same database and share tables, we have made our first step by splitting a large marketing service into coarse grained services based on functionality.
Now it’s time to eliminate coupling based on data. So, let’s see how our services map to data tables (in case of relational database).
Because we have shared database, we have some tables shared between different services. Luckily, the other tables are not shared, so we can easily split coarse grained services based on the data being mapped as shown below.
As result, we have tracking ads microservice separated. Great!
Next step is to take care of tracking leads service. It shares data with leads service, so it is not so trivial, but we have couple of options. Take a closer look at the shared table to see whether it belongs to leads or tracking leads service. If it’s neither, find a way to split the data amongst these two. In our case we decided that the data ownership belongs to leads service, therefore tracking leads service should stop accessing data directly and instead use leads service interface.
We have one more check to do. If tracking leads and leads services are too chatty, that is an indicator of too tight coupling. We need to go back to refine services granularity and do one more iteration of splitting based on data.
Since we have tracking as a subsystem, we may find ourselves in situation that managing interaction with tracking is difficult for our clients. In this case we need to introduce orchestration layer for tracking system. Orchestration could be just another service that communicates with a cluster of tracking services and provides clean and easy to use APIs.
Don’t forget that we got to deploy to production and route real traffic to our new tracking system. If possible, you may consider deploying to production more often, even some intermediary results. Note, that deployment to production and routing real traffic to new services are two separate tasks. You may exercise production deployment to get confidence in this process and not yet route real traffic to new services. However, the sooner you get real traffic hitting new services, the earlier you can get feedback and the cheaper it would be to change the course of action if any mistakes are revealed.
Reflect and repeat. Work through the other services, first splitting them to coarse grained services based on functionality, then mapping services to data and splitting further by data ownership. If there is any shared data identified, decide upon data ownership. Each service is responsible for its data, other can access the data through the service API. Next, check for tight coupling between services, refine by functionality and data again if tight coupling is present. Finally, think of orchestration level. Rinse and repeat over until the goals and objectives of migration are achieved.
Summary
Microservices is not “the” architecture, it’s one of the available architectures (along with monolith and service based) to choose from. Microservices migration has to be driven and justified by business value. Microservices approach requires people, processes and tools to be ready to operate in such environment, that doesn’t happen overnight. Migration is iterative process that benefits from incremental releases to production.
If you need to deep-dive into the subject, learn more migration approaches and pattern, below is a list of books that you will find very helpful. Good luck on your migration journey!
Reading List
Monolith to Microservices: Evolutionary Patterns to Transform Your Monolith, by Sam Newman. | |
Building Event-Driven Microservices: Leveraging Distributed Large-Scale Data, by Adam Bellemare. | |
Design & Build Great Web APIs, by Mike Amundsen. |
One thought on “Migrating Monolith to Microservice Architecture – Journey Continues”