Start by naming the actual trigger
Nobody migrates a working system for fun. There is a trigger: an unsupported platform, a database version going out of support, a supplier withdrawing a component, a compliance requirement, or the simple fact that changes now take weeks and everyone is afraid of the code.
Naming the trigger precisely matters, because it defines what success looks like and therefore what can be cut. If the trigger is an unsupported operating system, then a like-for-like port on a supported stack is a complete success and the wishlist can wait. If the trigger is that the business process has changed beyond what the system models, a port achieves nothing.
The most expensive migrations we see are the ones where the trigger was technical but the project became a redesign, because everyone attached their favourite improvement to it.
Inventory before design
Legacy systems are surrounded by things nobody remembers: a scheduled task on a desktop machine, a spreadsheet that reads the database directly, a report someone in finance runs every quarter, an integration to a system that was decommissioned but still receives data.
Before designing anything, build an inventory: entry points, scheduled jobs, integrations, reports, direct database consumers and the users of each. Then find the undocumented behaviour, which usually lives in stored procedures, triggers and validation rules written a decade ago for reasons that are no longer obvious.
This step is unglamorous and it is where migrations are won. Every item you discover after go-live costs several times what it would have cost to find now.
Choose a strategy honestly
There are three realistic strategies. Rehost, which moves the system to supported infrastructure with minimal change: fastest, cheapest, and it buys time rather than solving the underlying problem. Rebuild in stages, where new functionality is written alongside the old system and traffic moves across gradually. Or rewrite wholesale, which is occasionally right and always riskier than it looks.
Staged rebuild is the default we recommend for anything business-critical, because it gives you a working system at every point. The pattern is straightforward: put a boundary in front of the legacy system, move one bounded area of functionality at a time behind that boundary, and keep both running until the new one is proven.
Wholesale rewrite is defensible when the system is small enough to rebuild in a couple of months, or when the data model is so wrong that incremental change cannot fix it. Be suspicious of that conclusion if it arrives in the first week.
Data migration is a project inside the project
Legacy data is inconsistent. There will be records that violate the rules the new system wants to enforce: missing fields that are now mandatory, duplicates, dates in the future, values in free text that should be codes. You cannot discover this at the end.
Write the migration as repeatable code, not a one-off script run by hand. Run it early and often against a copy of production, and produce a reconciliation report each time: record counts, totals that must match, and a list of rows that failed with reasons. The reconciliation report is what earns you the confidence to switch over.
Decide explicitly what happens to the bad rows. Fix at source, transform on the way through, or park in a quarantine table for someone to handle. All three are legitimate. Silently dropping them is not, and it is what unattended migrations do by default.
Plan the cutover, and the way back
A cutover plan is a sequenced document with times, owners and checkpoints, and it should be rehearsed at least once against a copy. The rehearsal is where you learn that the export takes four hours rather than one, and that one integration partner needs two days' notice.
Every cutover needs a rollback with a decision point and a named person who can call it. That means keeping the legacy system able to resume until an agreed date, and being disciplined about the window in which returning is still possible.
Then keep the old system readable, not writable, for a period afterwards. Someone will need to check a historical record, and read-only access is far cheaper than a data extract request every fortnight.
The sequencing rule that matters most
Move the least risky, most self-contained area first. Not the biggest, not the one that annoys people most, not the one an executive is asking about. The first migration is where your team learns the process, the tooling and the reconciliation approach, and you want to learn that on something recoverable.
Then hold the sequence steady. Migrations lose momentum when the order is renegotiated every month, and a half-migrated system carries the cost of both worlds. Publish the sequence, note what each stage unblocks, and treat changes to it as a decision rather than a preference.
Plan for the whole thing to take longer than the technical estimate suggests, because the constraint is usually the availability of the people who understand the old system, not the writing of the new one.
If you are making this decision now, a half-hour call is usually enough to narrow it down. We will tell you what we would do and why, whether or not you hire us.
Book a free consultation