Transitioning to a miniservice architecture
Below are a couple of things to be aware of as well as a number of steps that can be taken in order to migrate a monolith to a miniservice architecture.
No common understanding and goal between teams and their individual members For the transition to succeed, it's vital for teams and their individual members to work towards the same goal as well as use the same strategy to get there. Think about what exactly you want to accomplish by transitioning to a miniservice architecture. Do you want shorter release cycles? Replaceability? Or do you just want to go with the latest fashion in architecture? Also, assess whether your team members possess enough knowledge about miniservices and their goals.
Keeping in mind Conway's law, you should take a careful look at your organisation and update any process or structure that wouldn't work well with running many independent teams.
Too-tightly-coupled parts If the monolith is not already set up in a modular way, transitioning to a miniservice architecture can become very hard. Refactoring the monolith to more clearly separate modules should be done first. Where possible, a module should only cover a single domain. Take care, though, as refactoring usually introduces unforeseen bugs.
Stateful to stateless transition Miniservices should not depend on persistent state and as such the monoliths functionalities should not depend on persistent state, apart from a loosely coupled database system or an abstracted-away cloud storage service. Miniservices depending on local storage are hard to scale horizontally, as that storage would need to be kept in sync. Miniservices should only access each other's data via APIs, so that each miniservices has clear ownership of their domain.
Integration testing after each split If a monolithic application is not already being integration tested before transitioning, these tests should be set up in advance. As refactoring oftentimes introduces bugs, you should assure yourself that the system works well in its entirety, before deploying. Where possible, automate these tests. Computers don't get bored with testing, humans might become less accurate over time.
Starting from scratch One might think it's always a good idea to throw away all legacy code in favor of a new architecture. It does give you the advantage that you don't have to live with the consequences of past decisions. However, accurately estimating the amount of work rebuilding a (large) application is hard and often very much underestimated. It's vital that the transition be manageable for it to succeed.
Steps to enlightenment
Set up integration tests As the application will be quite heavily in flux during the transition, a large amount of certainty in the correct functioning of the application as a whole should be created by systematically testing the system as a whole.
Gradually refactor the monolith, decoupling more functionalities within Decouple modules and domains as much as possible.
Select a simple and easy-to-isolate functionality of the monolith Don't go big. Single out fairly loosely coupled modules that are not too critical. Work in atomic steps: have only one module or domain be transitioned at a time. Give yourself time to learn how to migrate a part successfully.
Create a miniservice that has that same functionality Having identified a single task that can be transitioned, build a new miniservice that has this task as its single responsibility. Some guidelines to take into consideration:
- Avoid or at least minimize dependencies of the miniservice to the monolith.
- 'Avoid the anti-pattern of only decoupling facades: only decoupling the back end service and never decoupling data.' Tightly-coupled data slows down development and refactoring of individual miniservices. This can be done in a number of ways. Have each miniservice maintain ownership of private tables in a schema, set up one schema per service or, introduce a new database management system instance for each service. You could use the saga pattern to avoid database inconsistencies instead of two-phase commits.
- Prefer rewriting the functionality over reusing the monolith's code if at all realistic. This way, you can use the right tools for the job as well as update the process the functionality is used for. Watch out for the IKEA effect. Don't be afraid to throw away code you are proud of if necessary.
Test the miniservice and test it thoroughly This is vital: as monoliths might have a lot of interconnected functionality, finding out about introduced bugs should be done as early and thoroughly as possible.
Deploy miniservices somewhere deployment is easy Once the monolith is split up into many smaller services, running deployments becomes much more frequent. This should be taken into consideration from the start of the transition. Choose a simple per-service process and automate each step. You could look into setting up a kubernetes cluster, for example.
Connect the monolith with miniservice, and remove the functionality from the monolith in one go Omitting the removal of legacy code results in a harder to maintain the system as a whole.
Run the integration tests and update Test before deploying, and if possible, test the system with a limited number of real-world users as well before deploying to the whole user base.
Reflect on the process Make it easier for yourself and your team members to learn from your mistakes, in order to be able to tackle more elaborate transitions. Keep a list of problems you encountered and precautions you'll take to avoid them in the future.
Go back to step 2, until there is no more monolith left
We think that the monolith-first approach to setting up a system of miniservices is best. This approach gives you time to stabilize the requirements and identify the domains in which an application can be separated. Having established clear, small steps and beginning with easy migrations, gradually transitioning a monolith to a miniservice architecture seems the most sensible way.
Introducing a miniservice architecture involves a possibly drastic change in the way databases are operated. In my next blog post, this will be elaborated on in more detail.