PHP Monolith to Microservices Using the Strangler Pattern
Dealing with the legacy system can be a real pain, both for developers and for business owners. It comes with a handful of consequences: longer time-to-market, difficult-to-debug issues, performance problems, and higher development effort - just to name a couple of them.
Sticking to a monolithic architecture might also be one of the "legacy" issues to have. It usually hits bigger companies, where the development is split between multiple teams.
Legacy system - how it looks now
Let's quickly imagine a legacy monolithic application that was built when the company was smaller, and then extended rapidly as the company grew. Now developed by four teams that need to share the same codebase. As always (in legacy systems), there is spaghetti code, and the modules are tightly coupled. The overall software architecture is rather poor, and the amount of technical debt is high. The codebase is missing unit tests, and in general, far from being easy to maintain and extend.
In such situations, a change introduced by one of the teams might break the code maintained by another team.
When, for example, team III pushes some code that breaks things for team I, they actually block the entire project, as no other team will be able to release, unless the changes are reverted or the bug is fixed. What's more, quite often a bug in the team's own part of the project might block all other teams.
The legacy application is both build and released as a whole, so an issue in any part blocks the whole release process:
I've seen projects with 1-month feature freeze periods, just to be able to release anything to production. And even having the feature freeze, they had issues from time to time!
So in case your project is in such a condition or is moving towards this, you might want to split it into microservices, and let each team maintain its own development process, releases, etc.
New system - how it could look like
Let's step back for a moment from the old system, and focus on how a modern application could be organized. In projects that are split between multiple development teams, it might be beneficial to use the microservices architecture and benefit from each team being able to deliver business value faster in their own separate process. When designing the new architecture, each team could also ensure to lower the code complexity and make sure there is no performance bottleneck. The team is now not blocked by another team and can follow its own quality rules.
Extracting one part of the system already improves the process, and can make a huge difference:
Looks nice, doesn't it? The team is able to release new features on its own. You can now extract the next component and make it independent!
Now that we know what the end goal looks like, let's ask ourselves an important question.
Wait, why can't we rewrite this project from scratch?
The short answer is: it's pretty risky. All IT projects that are not split into small pieces are risky by definition. That's one of the reasons why Agile gained so much traction. By splitting your work into short iterations, you minimize risk and allow yourself to act quickly if something goes wrong.
Another reason is that in order to build a new system with good quality, you need to understand the business and the existing system very well. For large codebases, this gets tricky, cumbersome, and often leads to the bad design of the new system.
One more problem is that you need to wait a long time until the new system can be used. It often takes months or even years, during which your old application should be maintained. Such a combination is hard to manage: if you focus too much on the new system, the legacy application won't get the required improvements; on the other hand, if you focus too much on the legacy system, the new services will take ages to finish.
I've written a post on this topic some time ago, if you would like to learn more - check it out: 5 reasons why rewriting an application from scratch is a bad idea.
Ok, so how do I manage the migration process
If you are not familiar with the Strangler Pattern, I suggest reading our articles on it, f.e.: Strangler pattern approach to migrating applications - pros and cons.
But in short, The Strangler Pattern is a well-known design pattern to transform an old, legacy system, into a new one (in our current approach microservices), using small, incremental steps.
You select one set of features and create an implementation as a new service. So for a moment, you have two separate versions running.
When one part of the system is rewritten, you strangle the old implementation by switching all traffic to the new system. The traffic is routed to the correct implementation using a thing strangler facade - in most cases a simple reverse proxy.
You can rewrite your system part by part, eventually moving all features to the new application, completely replacing the entire application. You can even add new functionality at the same time!
Thanks to that, any upcoming changes to this (rewritten) part of the system will be done in the new, microservices-based code base. Because your new microservices are decoupled from the legacy system, you can follow Test-Driven Development, use newer tools, have a separate build/release pipeline etc. There are no major boundaries that would prevent you to apply the architecture and/or solutions you need.
The important question is: how to select which component to rewrite first?
- If you are new to the Strangler Pattern, please select a simple component, preferably not tightly coupled with the rest of your legacy application. Rewrite, make mistakes, and learn how to apply this approach. It will get more complex with bigger components, so gain some confidence first.
- Favor components that are well known, easy to understand, or have a good test suite. Again, this will help to gain some confidence.
- If you are confident with the Strangler Pattern approach, consider the following characteristics:
- how often a component needs to change - if it changes frequently, you will benefit more from moving it out of the legacy monolith;
- if there are performance problems with one of the components - moving it out will allow you to scale it separately;
- generic feelings of the team, about a component - they might struggle with some parts more and would like to get rid of some issues first.
There is no reason to rewrite a component that:
- works well;
- has a very bad code base;
- does not change at all.
As the risk of rewriting is high, it's hard to do it, and the ROI is almost 0.
Taking on the strangler path
The path from a Monolithic, legacy app to well-written and well-thought microservices is long and bumpy. Yes, it will get hard sometimes, but believe me - it gets way easier if you split it into smaller chunks.
Divide and conquer. Smaller parts mean less risk, easier development and better control of the whole process.
Another nice thing is that if you choose the components wisely, you will achieve great results in a short period of time. By extracting one of the components, you can release it frequently, without being blocked by other teams.
Now, as you can see, a migration from a monolithic architecture to microservices using the strangler pattern is pretty easy and straightforward. I'd dare to say it is too easy! Why? Because some projects take this path without considering the hidden costs and potential issues. Microservices are not a silver bullet, they won't work in all projects! I often see small companies with only one dev team implementing microservices. This is in most cases bad, and should not be done. Thankfully, the strangler pattern method can be applied also to other, simpler architectures, so no matter what architecture works for your system - the strangler pattern might help to get there!
Let me know if you have any questions, I'm always happy to discuss similar cases!