- Part 1
- This post
Now, where were we
This post continues the story from my previous post about vendor lock-in. The previous post wasn't very general in nature, and focused a lot instead on my specific woes; mostly talking about a problem, and not so much a solution.
Since then, I've read This interesting article over at IBM.com. It speak a lot to my issues, but its conclusions aren't entirely what I am going for.
It describes how to break a monolith into microservices incrementally using the strangler pattern which - in summary - is about moving vertical slices of the old monolith into self-contained services one at a time. Along the way, the monolith will lean on the new services, strangling its old implementations one at a time until only the new, fresh services remain.
That is all well and good, but one very important fact that is often overlooked is that microservices are not for everyone. The microservice pattern comes with its own set of issues and operational overhead, which - in many cases - are simply not worth the hassle.
What I am looking for is a way to break up and ol' ball o' mud into a proper, layered, modular architecture a little bit at a time.
Before going further, let's just quickly define what a Microservice and a Monolith means in the context of this post:
- Microservice: A small application handling a single business capability. It has its own lifecycle and controls its own database
- Monolith: A full application with all relevant business capabilities organized into a single executable. It may span multiple domains, and it also controls its own database, which will be much wider in scope than that of a microservice
Now, just reading these two descriptions may make you think that Microservices are an obvious choice; but consider the fact that these patterns mostly speak to how processes are organized during operation and not how the code is organized. A set of Microservices need to be handled independently and updated independently, while maintaining backwards compatibility to make sure they don't break the web of interdependencies. In contrast, a monolith is built and deployed as a single entity. Much much simpler.
Is a monolith always a messy blob then? Not at all! It can still be composed from a set of independently maintained modules/libraries. The main difference is that all these dependencies are managed at build-time instead of during execution in some bespoke environment.
So what am I going for exactly?
I have an old, messy monolith written in one tech-stack, and I want to turn it into a clean and organized, new monolith, written in another tech-stack. No further mention of Microservices in this post, so if that is what you came for: Sorry to disappoint.
Part 1 of this series of posts talked briefly about bottom-up and top-down refactoring, which can briefly be summed up like this:
- Top-down: focus on rewriting the user-facing systems and lean on the existing bottom layers. Once the user-facing side is cleaned up, we can do the lower layers
- Bottom-up: Without any functional change to the user-facing systems, rewrite the lower layers into a more organized system, and then build a fresh, new UI on top of it afterwards
There are benefits and caveats on both of them. Is it worth spending time doing lower layers, if it doesn't provide user value along the way? Are the bottom layers sensible enough that you can actually provide a great user experience first? Hard to say.
The referenced IBM article has a very good point when it talks about where to start:
An ideal starting point is to identify the loosely coupled components in your monolithic app. They can be chosen as one of the first candidates
If you are working with a ball of mud, where cohesion is low and coupling is high, then there is no obvious place to start. We would need to analyze the code-base in question and find the "seams" to work with. The bottom-up and top-down idea comes from this line of thinking, making the assumption that there is a relatively clean border between user-facing part and backend logic. In my own situation though, the more I look at it, the more I find that these concepts are not isolated enough that it would make sense to try either of them.
Instead of going for a vague top/bottom distinction, it makes sense to follow the contours of the code and see if we can find parts with low coupling and start there. In my specific case, that would be the Reporting feature, or the User management feature, because they are largely independent of the rest of the functionality.
So we've found where to start. Now what?
Now we need to figure out how to actually do it. It is hard to speak in general terms here, since the specific implementation strategy really depends on the code-base and the type of functionality we're talking about.
A general approach is somewhat well described in the IBM article, though:
Here is an example to understand this approach. [...] Module Z is the candidate that you want to extract and modules X and Y are dependent upon it. You can create an IPC adapter in the monolithic application that X and Y use to talk to Z with a REST API. With this adapter, you can move all of the modules from the monolithic app [...].
A REST API would be one way to go, but other IPC approaches might also work, depending on the setup. The key here is that - even without microservices - you have the freedom to break up chunks of the code and put it into a different process and then have the processes talk to one another. A simple approach would be:
- Rewrite feature A from the old app in a new application
- New application provides some means of API
- Old application writes an adapter to talk with the new application
- Probably build the two together and deploy in a single (or a few) docker containers, so they are always deployed in sync
To the developers, there are now two applications to maintain. One being slowly strangled, and the other growing; but to the users, there isn't really a difference. Still just the same application as far as they know.
The communication between the two applications need to be worked out once and once that is solved, it is just a matter of prioritizing which part of the old app to move over next. The communication layer does need to take some important things into account:
- Serialization/De-serialization of data and the performance implications
- Transactional integrity. If both apps modify the data, they probably won't be able to share the DB transaction, since they each maintain their own connections
- DB schema ownership. Only one of the applications should be in control of the database structure. It would be a case-by-case judgement call of whether the old or the new should be in charge. For my case, I think it makes sense to leave the control to the old application for as long as possible, just to not go overboard with fancy refactoring before any value if to be gained from it.
- Session sharing, if part of the UI is served from the old app and the other part from the new. If the user is logged into one, it should also be logged in to the other seamlessly
With all this out of the way, there is still the consideration of what to do with user-facing things and backend things. I think there are two ways to go about it, just like the top/bottom idea above:
- Do all the backend stuff one slice at a time, and once all that is dealt with, write a new UI for the users in one go and sunset the old.
- While moving backend stuff over one slice at a time, consider whether a group/section of the UI can be transformed into a new UI experience along the way, rather than in one go later.
Option 2 is something I've seen in a few web applications before. It was just business as usual until suddenly there was a little toggle at the top saying: "Try the new UI Beta". When I toggled the switch, it would basically take me to a different UI, which was not yet feature complete, but provided a nicer user experience. It was then up to me as a user whether I wanted the full feature set or the nice experience, and it let me get acquainted with the new UI before having it sprung upon me at a later time. They also added the ability to provide feedback for the new UI per page, right there in the app.
I think this approach has a lot of appeal. And it doesn't even have to be a completely new site, but could be one page/group/section at a time. For instance, if I toggled the Beta UI on, I would see the new version of UI whenever I navigate to a page where it is supported, and otherwise see the old version. This may make for a bit of an uneven user experience of course, but the control is in the hands of the user.
Summing up my thoughts
I think - for my specific case - I would first do a small proof on concept for the IPC adapter and see if I can have the two processes communicate in a reliable way, and once that works, just take a low-coupling, low-risk slice first and measure the impact and value. The rest of the ideas above can be added along the way as needed.
Most important part of all this is:
- I don't take on the full bulk of the work in one go
- I release user value along the way while dealing with the technical debt of the old solution along the way
Sure, I didn't spend many breaths here talking about the risks and downsides to all of this, among which could be:
- Two separate tech-stacks in one project? Is the team capable enough?
- Promise of greener grass on the other side, but most likely development will slow down for quite a long time while new, unforeseen problems are being dealt with
But for my own case at least, these two are no big concern.
For anyone reading this, I hope you'll drop me a comment and point whether there is something I have missed. It feels like this post rounds out what I was trying to say in my first post, but I'm sure more will need to be thought out as I start tackling these issues for real. I welcome any words of caution and advice in the matter.