Why it matters
Choosing the wrong migration pattern is a classic large-scale IT mistake. Rehosting everything wastes cloud value; rebuilding everything takes years and risks failure. The right pattern per workload is the goal.
The architecture
Rehost (lift-and-shift): move as-is to VMs in cloud. Fastest, cheapest migration but doesn't leverage cloud value. Good for time pressure or datacenter exit.
Replatform (lift-tinker-shift): small changes to leverage cloud managed services. Move DB to RDS, keep app on VMs. Better cloud value with less risk than refactor.
Repurchase: replace with SaaS. Retire the on-prem app entirely; buy Salesforce, Workday, etc.
How it works end to end
Refactor: substantial changes to leverage cloud-native services. Break monolith into microservices, use managed queues, adopt event-driven patterns.
Rearchitect/Rebuild: rewrite from scratch with cloud-native design. Highest cloud value but highest risk and cost.
Retain: leave on-prem. Some apps aren't cloud candidates due to latency, regulation, or unclear ROI.
Retire: turn it off. Migration is an opportunity to shed unused apps.
The six Rs name the destination, not the journey
The rehost / replatform / repurchase / refactor / retire / retain taxonomy earns its keep for exactly one reason: it forces a decision per workload instead of a single decision for the whole estate. What it does not tell you is anything about the work. Two systems that both land in the replatform column can differ by two orders of magnitude in effort, because the effort lives in the data, in the dependency graph, and in the cutover instant — none of which the taxonomy names.
Treat the label as a column in a spreadsheet, not as a plan. A plan is an ordered list of reversible steps with an explicit statement of what breaks at each one, and a named person who decides whether to proceed. When a programme stalls it is almost never because someone chose refactor where replatform was correct. It stalls because a payroll service turned out to read a fixed-width file off an NFS export nobody had inventoried, or because a 4 TB order-history table cannot be copied inside the four-hour window the business signed off on, or because the team that wrote the thing left in 2019.
The rest of this article is about those failure modes rather than the taxonomy. For the programme-level view — waves, gates, stakeholder management — see the migration architecture guide; what follows is the engineering underneath it.
The compute moves easily - the data does not
A stateless tier is close to disposable. You can build the target image, boot it, point it at nothing, delete it, and try again on Tuesday. Nothing is lost because nothing was held. That property is why the application layer of a migration tends to finish early and why it creates a false sense of progress.
A database has the opposite property. Its old copy keeps accepting writes for the entire duration of the project, and its new copy has to become an exact equivalent at one specific instant. That forces the work into three phases with three different failure signatures:
Bulk load. A consistent snapshot of the source, copied into the target. This phase fails on throughput and on translation — character sets, collation order, timestamp precision, unsigned integers, the column your source engine allows to be both NULL and empty string.
The replication window. Everything written to the source after the snapshot has to reach the target. This phase fails when lag does not converge: the target applies changes more slowly than the source produces them, and the gap widens for days without anyone declaring it a blocker.
The cutover instant. Writes stop going to the old system and start going to the new one. This phase fails on the things that could not be tested, which is why the surrounding sections spend more time on rollback than on the flip itself.
The asymmetry to internalise: the stateless tier rolls back by redeploying the previous artifact. The database does not roll back at all. Once the new copy has taken production writes, the old copy is stale, and returning to it means replaying a delta backwards through a system that was never designed to receive it.
Snapshot, log position, and the handoff that has to be exact
What makes the replication window tractable is a single mechanism: take a consistent snapshot, record the exact log position that snapshot corresponds to, and start streaming subsequent changes from precisely that position. The correctness of the whole migration reduces to that handoff being exact. Begin the stream a few records late and you lose those changes silently — nothing errors, the row counts still match, and one customer's address is wrong. Begin it early and you replay, which is harmless only if the apply path is idempotent: keyed upserts rather than blind inserts, and no UPDATE ... SET n = n + 1 anywhere in the transformation.
The engine mechanics — how the write-ahead log is decoded into row events, what a replication slot actually holds — are covered in logical decoding and change data capture. What that article does not cover is the operational hazard specific to a migration, which is that the capture position is a retention anchor. The source cannot discard any log segment newer than the position your consumer has acknowledged. If the target applies more slowly than the source writes — overwhelmingly likely during the initial load, when you are saturating the target's I/O with the bulk copy at the same time — the retained log grows without bound. Filling the log volume on the source takes production down. You will have caused an outage on the system you were trying to move.
So instrument three numbers from day one and alert on all of them: replication lag in seconds, retained log bytes on the source, and free space on the log volume. Decide in advance what the ceiling is and that you are willing to drop the capture and restart the bulk copy rather than breathe on the source. Restarting a copy is a bad week; filling the source's log disk is an incident report.
The other thing that moves under you during the window is the schema. A migration lasting weeks will overlap at least one deployment that adds a column. Freeze DDL on the source for the duration if you possibly can; if you cannot, the apply path needs to handle unknown columns without stopping the stream, and someone needs to own the target-side change.
Three cutover strategies, and what each one honestly costs
There are only three ways to get from an authoritative old store to an authoritative new one. Every vendor methodology is a variation on one of them.
Dual write
The application writes to both stores. It is the most popular choice with people who have not implemented it, because it looks incremental and requires no infrastructure. It is a distributed transaction wearing a disguise. Two writes are not one write: the first can succeed and the second fail, leaving divergence that nothing surfaces. Worse, two concurrent writers can land in one order at the old store and the opposite order at the new one, so both stores receive exactly the same set of writes and end in different states. No error is raised, because from each store's point of view nothing went wrong.
Dual write is only honest if it ships with a continuous comparison job and a repair path that you have actually run. It is defensible for append-only or naturally idempotent keyed data. It is a poor choice for anything involving read-modify-write, counters, or balances.
Log-based replication
One writer, one authoritative store, changes shipped from the log. Source ordering is preserved for free, and the application needs no change, which also means no application bug can cause divergence. This is the right default whenever it is available. It costs you privileged access to the source (log-level permissions many DBAs will resist), and it is unavailable on some sources entirely — older engines, appliances, and most SaaS. Where you are also transforming the schema en route, the apply side now contains logic, and logic contains bugs; that transformation deserves its own test suite against real captured events.
Read-only freeze
Declare the source read-only, let replication drain to zero lag, verify, flip. It is the simplest and by a wide margin the safest, and it is the one the business pushes back on. The engineering job is not to avoid the freeze but to make it short enough to be acceptable: everything is pre-copied, so the window covers only the drain and the verification, and a read-only window is a far easier conversation than a full outage — most users cannot tell the difference on a Sunday morning. If you can afford ninety minutes of read-only, take it over three weeks of dual-write reconciliation.
Strangler fig and the routing seam
Incremental extraction needs a place to stand. That place is a routing layer in front of the system that can decide, per request, whether the old implementation or the new one serves it. Without it you have a rewrite with a big-bang cutover, whatever you call it in the plan.
The router decides on whatever dimension your system actually partitions by: URL prefix, tenant identifier, customer segment, or a percentage. What matters more than the dimension is three properties. First, neither implementation may know about the router's decision — the moment the old system contains a branch for "if migrated", the seam has leaked and you cannot delete it cleanly. Second, the decision must be data, not code: flipping a route back has to be a configuration change measured in seconds, not a deploy of the router measured in a release cycle. Your worst-case rollback time is exactly the time it takes to change that value. Third, the seam has to account for state that both sides share — session tokens, identity, and above all identifier generation. Two implementations minting primary keys from two sequences will collide, and you will discover it after both have written.
routes:
- match: { path: /api/catalog/* }
target: new # migrated wave 1, 100%
- match: { path: /api/cart/* }
target: legacy
mirror: { target: new, sample: 0.05, compare: true }
- match: { path: /api/orders/* }
target: legacy # blocked on the order-history copy
default: legacy
The mirror line is the cheapest insurance in the whole exercise. Shadow a small percentage of read traffic to the new implementation, compare the responses, discard them, and serve the legacy answer. You find divergence on production data and production shapes without a customer being involved. The discipline required is that mirrored requests must be side-effect free — a shadowed request that sends an email or decrements stock turns your safety net into the incident.
Rollback, and why the point of no return arrives early
Ask a team when their point of no return is and most will name decommissioning the old estate. It is almost always the cutover instant instead. The point of no return is the first moment the old copy of the data stops being authoritative, because from that moment the two copies diverge and every additional minute of production traffic widens the gap you would have to reverse. Everything after it is forward-fix, whatever the plan says.
You can genuinely push that moment later, but only by building for it: reverse replication that streams changes from the new system back to the old one after cutover, keeping the old copy warm for hours or days. It is the same capture-and-apply problem with the arrows swapped, it is real engineering effort, and it is worth it for a small number of workloads and not for the rest. Decide deliberately rather than discovering during the incident that you assumed it.
What actually defeats rollback in practice is rarely the database. It is the ancillary state that crossed the boundary during the window: messages consumed from a queue and not replayable, webhooks already delivered to a partner carrying new-system identifiers, files written to the new object store, invoices sent. Before cutover, enumerate every outbound effect the new system can produce and decide for each whether it is reversible, idempotent, or permanent. The permanent ones are your true point of no return, and they usually fire earlier than the database does.
Then drill it. A rollback procedure that has never been executed is a document, not a capability, and the gap between those two is discovered at 3 a.m.
Dependency discovery, and why the map is always wrong
Every migration begins with an inventory assembled from documentation and interviews, and that inventory is around seventy per cent complete on a good day. The missing portion is not randomly distributed — it is precisely the things nobody remembers because nobody has touched them, which is also a decent definition of the things that will break. The recurring shapes:
A cron job on a host with no owner that writes a CSV some finance report reads. A hard-coded IP address, which lives in three places at once: an application config, a firewall rule, and a partner's allowlist you have no visibility into. A shared filesystem mount that two applications quietly use as an IPC channel because someone needed to pass a flag between them in 2016. A batch job that runs at quarter end, and therefore does not appear anywhere in your four-week observation window.
Observation beats interviews. Flow logs and connection tables sampled over time, DNS query logs, and the source database's own view of which hosts hold connections will each surface callers that no human named. Run that collection for at least one full business cycle, and prefer a quarter to a month if the calendar allows it. Anything that still resolves the old system by address rather than by name is a landmine; moving everything onto names before you move anything is the cheapest de-risking available and can be done months ahead of the migration itself.
Identity is its own dependency and deserves the same treatment: service accounts, machine credentials, and the trust relationships between them rarely map one-to-one onto the target's model. The cloud IAM article covers that model; the migration-specific work is producing the mapping table from old principal to new one, and confirming that no workload authenticates by an implicit property of the old network.
Licensing and data gravity choose the pattern for you
Two constraints routinely overrule the pattern the architecture review preferred, and both are usually discovered late.
The first is licensing. Where the source engine is licensed per core, a like-for-like rehost can cost more than the managed equivalent purely because the core count changes on different hardware, before anyone has evaluated a single technical merit. Bring-your-own-licence terms may require dedicated hosts, which changes the target topology, the failure domain, and the operational model in one stroke. Read the mobility terms of every commercial component before choosing the pattern, because the answer sometimes converts a rehost into a repurchase or a migration to an open-source engine, and that is a completely different project.
The second is data gravity, which is the observation that whichever side holds the large dataset attracts the compute. Move an application to the target and leave its 40 TB warehouse on-premises and every query now crosses a wide-area link; your tail latency becomes a function of round-trip time and result size rather than of anything you optimised. This is the classic cause of a migration being rolled back after a week.
The measurable tell is chattiness. An application issuing three hundred small queries per page render is entirely comfortable with a 0.3 ms hop and completely broken by a 30 ms one — the same code, ten thousand times the wait. Count queries per request before you split a tier across the boundary, and let the count decide the order of operations: move the data first, move both together, or do not move. The connectivity itself — tunnels versus dedicated circuits, and how to design the hybrid path — belongs to the cloud networking article.
Bandwidth arithmetic, and when the wire stops being an option
The bulk load is a physics problem before it is an engineering one, and the arithmetic takes a minute. Work an illustrative case: 100 TB to move over a 1 Gbps link, and assume you sustain 70 per cent of nominal after protocol overhead and contention.
payload 100 TB = 800 Tb = 800,000 Gb
effective rate 1 Gbps x 0.70 = 0.7 Gb/s
elapsed 800,000 / 0.7 = 1,142,857 s = 13.2 days
same payload on a 10 Gbps link -> 1.3 days
but: the link also carries production traffic, so if you
cap the copy at half the circuit, double both numbers.
The number itself matters less than the comparison it enables. Set the elapsed transfer time against the rate at which the dataset changes. If the copy takes thirteen days and the source churns two per cent per day, you are chasing a moving target and log-based replication is not optional — it is the only thing that closes the gap. If the copy takes six hours and the source is quiet overnight, a freeze window may be all you need and you can skip the capture pipeline entirely. That single comparison determines most of the design above it.
When the wire loses, the answer is physical transfer: write the data to a shipped storage device and have it ingested at the far end. Evaluate it end to end rather than by the device's rated capacity, because the effective rate is the payload divided by encrypt-plus-copy-out, plus transit, plus ingest, plus verification — and the transit leg costs days regardless of how much you put in the box. That shape is why physical transfer wins decisively at large volumes and loses to a modest link at small ones.
Two things it does not do. It does not remove the replication requirement: everything written while the device is in transit still has to reach the target over the network, so you need the capture pipeline anyway. And it does not remove the verification requirement — checksum at the source, checksum after ingest, and compare, because a shipped device is one of the few parts of your migration that can be physically dropped.
What rehosting actually costs you afterwards
Lift-and-shift has a bad name and deserves roughly half of it. What it buys is real: a datacentre exit on a fixed deadline with the smallest possible number of simultaneous changes. When the lease expires in nine months, changing the hosting location and nothing else is not a failure of ambition, it is competent risk management. Every additional change you make at the same time multiplies the number of hypotheses you will have to test at 2 a.m.
What it costs is that you now run the old architecture and pay a new bill. Concretely: the instance is sized to the peak of a five-year-old sizing exercise, and it runs continuously because the physical machine always did. The database is self-managed on that instance, so you still own patching, backup verification and failover drills, and you now also pay for provisioned storage and IOPS. Scaling still means resizing, which still means a maintenance window, so the elasticity that justified the programme never materialises. The operational model — hosts, SSH, configuration management, host-level monitoring — survives completely intact, which is why none of the promised operational savings appear.
The failure mode is not rehosting; it is rehosting and then stopping. Rehost as a funded phase one with a dated phase two, and be honest in the business case: if phase two is unfunded, the number to plan against is the run rate of the old architecture continuing indefinitely. Track it as its own line rather than as a rounding error — see cloud FinOps for how to make that number visible to the people who can act on it.
The cheapest single improvement on arrival is rightsizing, because the source machine's specification encodes a purchasing decision made under a procurement cycle, not a measurement of demand. You now have per-instance utilisation data you never had on-premises. Use it in the first month, while the migration still has attention.
Who operates it on Monday - the runbook gap
Cutover does not end the migration; it moves the system into an environment whose failure modes the on-call rotation has never seen. This half of the project is consistently under-planned because it does not look like engineering.
Start with the runbooks, all of which are now partly false. They name hostnames that no longer resolve, consoles that no longer exist, and procedures whose steps have changed shape: "restart the service" used to be an SSH session and a service-manager command, and is now a control-plane action with different permissions, different audit consequences and a different blast radius. Rewriting them is not optional, it is not free, and it should be scheduled as migration work per workload and made a gate on cutover rather than a follow-up ticket that ages out.
Then settle ownership explicitly. Where a central platform team executed the move and a product team inherits the result, the handover has to name who holds the pager, what the escalation path is when the platform layer is implicated, and which alerts still mean anything. That last one bites hardest: thresholds were calibrated against specific hardware, and disk latency, CPU steal, network variance and garbage-collection behaviour all shift on a different instance family. A rotation that spends its first fortnight acknowledging meaningless pages stops reading them, which is a worse outcome than having no alerts.
Buy the confidence cheaply by running at least one game day in the target before cutover. Kill an instance, fail the database over, let a credential expire, and watch what the team does. Everything found there costs a morning; the same discovery after cutover costs an incident and a difficult conversation.
Measuring a migration honestly
"Workloads migrated" is a project metric. It counts effort spent, not value delivered, and it rises whether or not a single system got better — which is exactly why it appears on every steering deck.
The honest measurement is a before-and-after comparison at the same traffic shape, on a small fixed set:
Latency at p50 and p99 for the two or three business transactions that actually matter, not an average across all endpoints, which averages away the regression you need to see. Error rate measured at comparable load rather than in aggregate, since a quiet week flatters everything. Cost per transaction rather than total cost — total spend moves with volume and can be argued about forever, while the unit number cannot; cloud FinOps covers how to build a defensible denominator. Time to recover from a defined, drilled failure. Change lead time, because "we can deploy on a Wednesday now" is frequently the largest benefit and the one nobody records.
All of these require a baseline captured on the source before anything moves, over at least one full business cycle. This is the step almost every programme skips, and skipping it converts every subsequent argument about whether the migration worked into an exchange of anecdotes. Instrumenting the old system feels like wasted effort on something you are about to delete; it is the only thing that will let you prove the result.
Finally, report regressions as regressions. A migration that raised p99 by forty per cent and cut cost per transaction by fifteen is a trade, not a success. Stating it that way is what earns you the budget to fix the latency, and hiding it is what causes the next migration to be run by somebody else.