How can a monolith be deconstructed efficiently?
Over the past two years, the engineering team I lead has been driving a monumental effort, transforming a 15-year old monolith application and rebuilding it as a microservices platform.
We chose to use microservices to rebuild our monolith application because it provided the flexibility to scale our platform as well as independent apps and services that can be owned and managed by our product engineering teams. Microservices also offered us a foundation that we could use to incrementally add new functions to the site without worrying about breaking existing ones.
Our legacy monolithic application had served us well, but we were unable to make the major functional and architectural changes we required to meet the needs of our customers and sellers. At the same time, we were finding it increasingly difficult to attract engineers with the necessary Ruby on Rails skills to enhance the application.
While we managed to increase the mileage of that platform by using solutions like AWS and containerization technologies for around five or six years, we still weren’t able to bridge the major feature and capability gaps, or deliver an optimal user experience.
How we accelerated our monolith to microservices journey
The journey hasn’t been easy, but there were some core strategies and methodologies we used to make the transition much smoother, increasing the rate of delivery while minimizing the amount of risk.
Controlling risk with the strangulation pattern
One of the main challenges we faced when transforming our monolithic application was controlling risk. Incrementally rebuilding new services and experiences, and decommissioning elements of the legacy application, was a complex and high-risk process. To limit the amount of risk in transforming the monolith, we used the strangulation pattern.
The strangulation pattern is a technique for re-platforming legacy systems where you incrementally rebuild service functionality into a new service and then ‘strangle’ or decommission the old function from the monolith once the new service(s) is ready.
We built our service-aligned API ‘façade’ over parts of the legacy solutions, and built the desired user experience, while integrating with the legacy solutions to deliver some or all of the existing functionality. This approach provides a new target set of APIs and allows you to add new features while minimizing your initial implementation. Once the experience is re-platformed, we then go on to iteratively rebuild all of the legacy capability into the new services. These new components were independently deployed from the monolith and insulated against any disruption whenever we deployed new services.
Deploying new services in this way reduced the risk of breaking existing functionalities when we deployed new services. In the future, this will enable us to continuously update our applications while minimizing the chance of compromising performance.
Enhancing developer experience and choosing active languages
The second key factor in the success of our strategy was our focus on cultivating a first-class developer experience. In practice, that means ensuring our developers are using modern technologies and tools, and are collectively using the best engineering and delivery practices. This creates a fulfilling working environment that helps increase retention and attract external talent.
Moving away from Ruby on Rails to Java, NodeJS, and ReactJS/NextJS was a critical step for getting the right developers because it opened the door to a much wider pool of talent. As a consequence, we increased our chances of onboarding exceptional engineers who could find ways to build our product.
From that point onward, throughout our entire transformation journey, we chose tools that were new enough to excite new talent and then consulted with our engineering community to ratify those technology choices.
This approach has worked because it has made all of our engineers partners in the transformation process, and given them the power to veto tools that could slow productivity. Ultimately, if they're dissatisfied with the tools we're using, it's going to take much longer to solve the problems we're trying to tackle.
Optimizing the user experience with the monorepo pattern
Another issue we needed to address was optimizing the browser experience for visitors to the site. This was incredibly important because, as an e-commerce site, we have to deliver a high-performance experience with pages that load quickly, or risk losing customers.
Similarly, we also needed a solution to maintain the consistency of that interaction and eliminate anything that could be a barrier to conversion. In this case, consistency is the coherent look and feel a customer gets when they interact with our site, such as a header, footer, drop-down menu, product carousel, or rich content. All of these interactions need to be responsive and consistent to satisfy our customers.
So, to meet these performance demands and optimize the user experience, we leveraged a monorepo pattern. This meant we developed a single monolithic repository to store all parts of the site code, but with build and deployment pipelines that only built, tested, and deployed components that are affected by a change. This meant that whenever we made changes to a single page, we only needed to deploy the affected page, making it easier to complete core framework changes where all components need to be updated.
Making incremental changes in this way has enabled us to maintain optimal performance, reduce the risk of delivery, and increase the rate of delivery. It's also helped us to support other core business objectives, such as enhancing our SEO posture by enabling pages to load faster.
Minimizing risk is vital for transformation success
Across the board, controlling risk has played a big role in the success of our overall transformation strategy. From using the monorepo pattern for our web applications, and the strangulation pattern to make incremental changes to our services, to increasing support for our developers so they stay with us long term; most of our choices have served to control risk, both to the end-user experience and the transformation as a whole.
In any digital transformation strategy where you’re deploying new technology solutions, you never know whether a solution is going to work as intended until you deploy it. Our experience has shown that embracing technologies and methodologies that provide a consistent framework for controlling those risks makes it much easier to avoid these pitfalls and transform sustainably.