I’ve spent the better part of my 20 year career modernizing and updating existing systems. Often the legacy system is valuable both to the company as well as to its users, but has outgrown the tech stack and engineering practices that made it successful a decade or more before. Unless the system is very small, it is advisable to modernize iteratively, shipping replacement functionality while maintaining business as usual. Although this is less risky than a “big bang” release, there are still risks involved in a multi-year modernization project. The best way to mitigate risks is to take them head-on by addressing the worst technical debt first, breaking up the monolith as quickly as possible and building a strong foundation for future development.
The classic formula for iterative modernization is Martin Fowler’s Strangler Fig Application. While engaged in conversation about modernization with some colleagues, I realized that the metaphor of tangled vines crowding out the original tree was at odds with what I really wanted to end up with. Fowler wrote in 2004 that his aim was “to gradually create a new system around the edges of the old.” This can be a great way to improve targeted areas of a system, but when it comes to complete replacement, Strangler starts to falter in a few ways.
It’s common in legacy systems to have components that read and write from the same legacy database and share the same domains and tables. The legacy database may not support our growth paradigm. It may be a performance bottleneck. Teams that attempt to follow the Strangler Pattern may defer this problem in favor of modernizing “edges” and may find they’ve burned up too much time, money and goodwill to complete the project. Solutions involving duplicative data stores and sync operations may dramatically increase the complexity of the system and cause all manner of operational issues.
The problem I have with Strangler is it doesn’t foretell the end state of the system and so we may wind up with a new system that is unnecessarily complex or that doesn’t align to our business goals. The objective of Strangler is to replace the old system with a new system which, we assume, will be superior and worth the investment. But judging by what some people are saying, it doesn't always play out that way. Knowing how to do an incremental rewrite is a great start, but it's not enough. We need to imagine what the system will be like to operate once it’s fully modernized. There’s no guarantee that iteratively modernizing “edges” will organically lead to a cohesive and runnable system.
So let’s get back to the metaphor. To successfully modernize a system, it is essential to envision the fully modernized system, how it will be operated, and how to support current and future business processes. Here we can turn to the excellent Team Topologies and be mindful of Conway's Law. When our system is fully modernized, we want our monolith to have been broken into well-defined, logical applications and services that match the missions of stream-aligned teams and do not exceed the cognitive loads of these teams. We want to maximize each team’s ability to build, test and ship their applications and services in independent streams.
So keeping the metaphor well within the realm of horticulture, I want to split up my application like a sliced apple.
The core goes to a team that builds shared services. This may include a core domain service, but also would develop cross-cutting solutions such as document storage/generation, email, monitoring, analytics, CI/CD pipelines and whatever else we don’t want each team to develop in silos. Each slice of the apple is a stream-aligned business process or other essential service that requires a dedicated team.
So how does this work? I advocate doing the hardest thing first and that will often be to build that common domain service and other horizontal or shared concerns. This could be hard work that touches a lot of legacy code but it is a critical step in modernizing our data layer and providing a strong foundation for the rest of the work. Doing this will pay off when we don't need to keep multiple databases in sync or repeat ourselves as horizontal functionality invades our vertical modernization initiatives. Often teams involved in work of this type will hope that clean domain services will “emerge organically” but in reality they wind up polluting the horizontal concern with logic specific to the vertical stream.
For example, consider a user roster. The roster lists users, supports grouping and filtering, bulk actions (including email) and can export pdf reports. If this is treated as an “edge” in a Strangler modernization, you could wind up with a vertical roster service that has all the capabilities mentioned above. Even if the team recognizes that things like emails and pdf reports are horizontal concerns, they will need to be extremely disciplined to avoid implementing them in the context of the current roster modernization project. It’s much better to produce well-defined services that can provide this functionality to the entire platform as a horizontal, then build the vertical on top.
With our legacy code refactored to use our new domain service, it's time to break up the monolith. With the worst of the tight coupling extracted into a domain service, we can now isolate "slices", or logical apps and services, within the monolith. The strategy for isolation will vary from project to project, but if possible, isolating slices into their own code repositories with their own deployments is ideal here. This, after all, mirrors our desired end state.
To recap, we’ve built some shared services, cut over to using them and isolated some of our features in the monolith. We may have additional goals like a UI rebuild or eliminating all the old code. Now is the time to focus on that. In fact, we’re incredibly well-positioned to get to the heart of modernizing these slices and we can have a high degree of confidence in our eventual success. It’s important to have a way to blend legacy and modernized user interfaces into a cohesive user experience. Perhaps I’ll write about how my team solves this problem in another post.
In summary, to contrast the different approaches:
- Vertical slices
- Monolith is broken gradually
- Some work is exploratory
- Data duplication via EventInterception
- Can start with a lower-risk component
- One big horizontal slice, then vertical slices on top
- Monolith is broken as soon as possible
- Desired end state is known
- Seeks to avoid data duplication
- Fail fast by addressing highest risk first
The ideas expressed here should serve a team well when they want to modernize a monolithic system as well as its monolithic database, have good knowledge of the system they are going to be modernizing and have some vision for how teams will continue to iterate on the new system that is better aligned to business streams. If that doesn’t describe your team, then a more targeted modernization may be warranted. After dealing with a few edges and becoming more comfortable with the system, the team may be prepared to tackle the core.
Cover image - "Galette; Apples" by 427 is licensed under CC BY-NC-SA 2.0