Out With the Old: 3 Steps for a Successful Software Migration Plan
Guest post by Staff Engineer and System Design Expert, Maxi Ferreira
📣 Integrate API users 50% faster (Sponsor)
Creating a frictionless API experience for your partners and customers no longer requires an army of engineers.
Speakeasy’s platform makes crafting type-safe, idiomatic SDKs for enterprise APIs easy. That means you can unlock API revenue while keeping your team focused on what matters most: shipping new products. Make SDK generation part of your API’s CI/CD and distribute libraries that users love at a fraction of the cost of maintaining them in-house.
Hi fellow High Growth Engineer, Jordan here 👋
Today’s article features Maxi Ferreira, Staff Engineer and System Design Pro. Maxi built the best frontend system design resource I’ve seen and he made it entirely free. He’s also featured in popular dev YouTube channels like LearnWithJason.
In this article, Maxi will teach you how to tackle one of the most common problems all software engineers need to deal with—migrations.
Without further ado, I’ll pass the mic 🎤 to Maxi 👏
Codebase migrations are painful. They’re risky, time-consuming, and there’s a high chance they’ll blow up in scope.
Even worse, once you finally finish the migration, you might feel that nothing really changed for the better–that all your hard effort was pointless.
Over the past 15 years, I’ve had the opportunity to lead codebase migrations of all shapes and sizes—sometimes successfully, and other times… not so much. If there’s one thing I've learned from all these experiences, it’s that the clarity of your migration plan determines your chances of success more than anything else.
So if you want to give yourself and your team the gift of a smooth migration, this article is for you. We’ll cover what I consider to be the three fundamental steps for crafting an effective migration plan so that your next project is as successful, enjoyable, and frustration-free as possible.
Let’s dive in.
⭐ What you’ll learn
How to define clear outcomes and metrics so that you can measure the success of your migration (and impact you made)
How to break down the work into manageable pieces so that you have a step-by-step plan that accounts for the risks
How to create a migration plan that will improve your chances of success by 10x
1) Define the outcomes you want to achieve
Before we start talking about planning a migration, it helps to take a step back and consider why we need one in the first place.
It can be tempting to approach a migration based on the fact that a codebase is too old, or too complex, or that it’s just generally "annoying to work with." But unless we can point to the outcome we’re hoping to achieve, we probably don’t have a good enough reason to embark on such a massive project.
For instance, some of the outcomes that drove my own codebase migrations in the past include:
Improving engineering velocity: All software systems accumulate complexity which slows down developers. There are typically lighter-weight options for improving engineering velocity, but in some cases, a migration makes the most sense.
Improving system performance: When small performance improvements aren’t enough to meet the system’s targets, it might become necessary to migrate it to a different language or framework altogether.
System requirements: You might need to migrate due to external infrastructure and 3rd party vendor changes.
Your desired outcome will also determine the metrics that measure the success of your migration. For instance:
If your goal is to improve engineering velocity, you could track metrics like cycle time, deployment frequency, or change lead time.
If your goal is to improve page-load performance, you could track user experience metrics like Largest Contentful Paint or Interaction to Next Paint.
If your goal is to reduce your real-time infrastructure costs, you could track your monthly bill from your cloud provider, or the number of concurrent real-time connections.
These metrics are typically a lagging indicator of your migration efforts because they measure something that happened in the past, so make sure you keep track of them before, during, and after the migration.
Metrics play an important role in your performance as well. Without clear metrics, you won’t be able to speak to its impact during performance review conversations.
Earlier this year, my team and I were mildly frustrated by the time it took to make certain changes on our frontend layer. The problem wasn’t that the codebase was old or poorly designed, but that it was too fragmented across multiple repositories—even one-line changes required us to coordinate multiple releases and open multiple PRs to update our various internal packages.
To relieve our frustration, we decided to migrate most of our frontend repositories to a monorepo with the clear outcome of reducing the cost of change. We tracked metrics such as cycle time, number of PRs created per ticket, and average PR size, and as we expected, we saw all of them go down in the months following the migration.
These changes made the team feel more productive, but thanks to a clear outcome and metrics, we had hard evidence of the migration’s positive impact on our team’s productivity.
2) Decide how to break down the work into manageable pieces
There isn’t a single right way to approach a large codebase migration, but there is a wrong way to go about it–doing it all at once in a single, Herculean effort.
Like with any large project, one of the keys to a successful migration is breaking down the work into manageable pieces. But that’s not so easy—how do we even know where to start breaking things down?
The short answer is, it depends. For instance:
If you’re migrating an e-commerce web application, you could start by breaking it down into different modules, such as the authentication module, the checkout module, the shopping cart module, and so on.
If the codebase spans multiple layers of the stack, your starting point could be the frontend, backend, and data layers.
If there are areas of the codebase that could benefit from being migrated earlier (perhaps because they’re the ones that change more frequently), you could split your codebase into high, medium, and low-priority areas and use them as your starting point.
The important part is that you break it down somehow and plan the order.
To do that, measure the parts you need to migrate on three axes:
Risks: Consider working on areas with big unknowns first to reduce risk early.
Impact: Prioritize migrating the areas with the highest ROI.
Effort: If all else is equal, migrate the easiest pieces first to make progress and keep momentum.
Map out each area that needs to be migrated onto a table like this one. Follow the zig-zag pattern to get the order you should do your migration in.
Below is an example using actual domain areas you might need to migrate:
Once you know where to start, you can continue breaking down the work recursively until all of the pieces feel manageable. By “manageable,” I mean:
You understand the scope of each one of the pieces, and
You’ve identified any high-priority risks associated with them.
For example, in the monorepo migration I mentioned earlier, the starting point for our breakdown was the list of repositories we wanted to bring together.
Some of these repos were small and well-encapsulated, so we didn’t have to break them down further. But the scope of the work wasn’t as clear in our larger repositories, so we had to continue breaking them down into smaller pieces until we felt comfortable with each one.
Considering the risks of each of your tasks can also help you decide the order in which you should approach the migration. For instance, you might choose to migrate the areas of the codebase with the biggest risks first, so that you can reduce the number of unknown unknowns throughout the rest of the project.
You’re not looking to mitigate risks at this point–only to identify them. You should understand the risks associated with each piece of work, but you don’t have to continue breaking down the work until each tangible piece is completely “risk-free.”
3) Communicate your plan with a migration document
So far, you’ve…
Defined the outcomes you want to achieve
Decided what metrics you’ll track
Broken the work into manageable pieces
It’s time to bring it all together in a migration document. A migration document is a technical design document that communicates your vision with the rest of the organization.
A migration document is an informal document, so there isn’t a standard format you need to follow, but in general, include at least the following sections:
Outcomes: What are the goals of your migration? Which metrics are you going to use to measure success?
Milestones: Based on your task breakdown, what are the different stages and your best time/effort estimate for each?
Risks: Which are the highest-priority risks you’ve identified, and how likely are they to happen?
Post-migration: What guardrails and constraints will you put in place to future-proof your codebase once the migration is done?
Rollout plan: How will you release the migration to your customers? Will you deliver each piece of the migration incrementally, or will you roll it out all at once when it’s 100% completed? What is your rollback strategy in case things go wrong?
Think of your migration doc as a starting point to set a general direction—not a step-by-step detailed plan.
As you execute your migration, you’ll learn new information that might make you re-evaluate your original plan. When that happens, incorporate your learnings back into the plan rather than charging ahead with your original one.
My team and I had a great plan when we started our monorepo migration. But soon after we moved the first repository to our shiny new setup, we realized we missed a critical step—we didn’t have a way to keep our git history.
Luckily, we found out about this early in the migration. So we reverted our changes, created a script that allowed us to move files across repositories while preserving our commit history, and added instructions on how to use it to our migration plan.
For large migrations, be ready to repeat this process of learning, re-evaluating, and updating your plan at least a few times. It takes effort to keep your plan updated, but your migration will have a much better chance of success.
I created an example migration document here if you want to see one. Feel free to use it as a template for planning your next migration.
📖 TLDR
Define the outcomes you want to achieve
Clearly state the outcomes you’re hoping to achieve with your migration. It could be improving developer productivity, improving the app’s performance, reducing costs, or a combination of those.
Define the metrics you’ll use to track the success of your migration.
Decide how to break down the work into manageable pieces
Find a starting point in the system you’re migrating from, and continue breaking down the work until all pieces are manageable.
You can consider a task sufficiently broken down once:
You understand its scope, and
You’ve identified the risks associated with it
Communicate your plan with a migration document
Your migration document is a technical design document that includes:
Outcomes
Milestones
Risks
Post-migration efforts
Rollout plan
Treat your migration document as a starting point to set a general direction, and incorporate your learnings back into the plan whenever you learn new information.
🙏 Thank you to Maxi
Jordan here again 👋
Thank you to Maxi for sharing his insights on how to set yourself up for a successful migration. Maxi spent an enormous amount of time on this article with me, putting an extreme amount of polish, and I hope it showed throughout the article.
I highly recommend following Maxi on LinkedIn, joining his newsletter, Frontend at Scale, and checking out his free Frontend System Design course.
👏 Shout-outs of the week
How to have 27 hours in a day on
— Productivity tricks that work. My favorite from this article was “Combine” and seeing the “Ab exercises + baby time” 😂Leading vs. Lagging Indicators — short article on an important product thinking concept. Work backward from the output you want, but know what your leading indicators are for that output. Some mentioned are awareness, adoption, engagement, satisfaction, and feedback.
- — Practical strategies for network building and increasing your chance of landing interviews. My last 2 jobs came from referrals.
Thank you for being a continued supporter, reader, and for your help in growing to 77k+ subscribers this week 🙏
You can also hit the like ❤️ button at the bottom of this email to help support me or share this with a friend to get referral rewards. It helps me a ton!
Thanks for the shoutout Jordan! Glad you enjoyed the real example 😂
I found that too often a PoC is missing in migration projects. Even assuming you decide to start with a small part (whether it's a microservice/page/whatever), the underline assumption is often that there will be next steps, and there is no pause to reflect how it went.
I try to always start with migrating the smallest thing we can to production, and then pausing to assess how it went. How long did it take, versus how long did we think it'll take? What do the developers feel about it? Often it's hard to rely on data at this stage because as you mentioned it's a lagging indicator, so I'm ok with relying on conversations to decide if the PoC was a success.
Only after the small part is completely finished and released, I think it makes sense to plan the whole project. You'll learn so many things during that small part!
Great stuff. The great thing about those 3 steps and it’s a foundation for how to execute any project, not just a migration project. Appreciate y’all sharing!