5 min readagentic engineeringdeliveryai strategy

Migrate the legacy service with agents, seam by seam

Pin the old service's behavior as characterization tests, cut the work into checkable units, run old and new behind a flag, and delete the old path on a named date, not "eventually."

By JP LeBlanc

An old rectangular service block on the left connected by parallel dual tracks, one behind a flag gate, to a new service block on the right, with a calendar node marking the date the old block is deleted.

Migrate a legacy service with agents by doing the part agents are bad at first: deciding what "correct" means. Pin the current behavior down as characterization tests, cut the work into units small enough to check one at a time, hand the mechanical translation to an agent, and run both versions in production behind a flag until they agree. The order matters more than any other single decision in the project.

Picture a return-processing service nobody wants to touch. Eleven years old, four owners since the person who wrote it left, and a pile of conditional logic nobody can explain without saying "I think that's for the Canadian tax thing." Everyone agrees it needs to move off its current database. Nobody has moved it, because the risk was never the code. It was not knowing what would break.

Characterization first, because this is the whole game

The instinct is to start writing the new version. Resist it.

A characterization test doesn't check whether the old service does the right thing. It checks what the old service actually does, including the parts nobody would defend if you asked them directly. Every weird branch. Every tolerated malformation. The response that would surprise you if you saw it cold. Run the current system against a wide slice of real inputs, record the outputs, and turn those observations into assertions before a single line of the new version exists.

This happens to be exactly the kind of work agents are good at: bounded, mechanical, and checkable against something concrete rather than against a person's opinion. Point an agent at the old service and a representative set of inputs, and it will generate a large characterization suite faster than a team would write one by hand. It doesn't need to know whether the behavior is right. Only what it is. That's a much easier bar, and it's the one that actually protects you.

Skip this step and you haven't done a cheaper migration. You've moved the discovery of every edge case from a test suite into production, where each one costs an incident instead of an assertion.

Cut it into units small enough to check

A migration described as "move the service" isn't a plan. It's a hope with a deadline attached.

Break it into units with a real boundary: one endpoint, one data transformation, one background job. Each unit gets its own characterization tests, its own agent-authored translation, and its own pass or fail, independent of the others. A unit that fails stays in the old system while the rest of the migration keeps moving. Nothing about a stalled unit should block a working one.

This is also where blast radius does its usual work. A failed translation of one endpoint is a discarded attempt and an afternoon. A big-bang rewrite that fails halfway through is a rollback of everything, including the parts that were fine, and a very uncomfortable conversation about why the whole thing is on hold.

What the agent gets, and what it never gets

The agent gets the translation: take behavior that's already been pinned down and reproduce it in the new stack, in the new language or framework, against the same assertions. That's interpolation, not invention, and it's the part of this job that was never worth a person's morning.

What it doesn't get is the seam. Where one unit ends and the next begins. Which edge case actually matters to a customer and which one was a mistake nobody ever noticed. Whether the undocumented behavior in that Canadian tax branch is load-bearing or an artifact of a bug from 2019 that everyone has since built workarounds around. Those are judgment calls about your business, made by someone who can be asked why, and an agent handed the same ambiguity will resolve it with whatever seems statistically reasonable, confidently, in whichever direction is wrong exactly often enough to matter.

Draw that line explicitly before the project starts, not implicitly by whichever engineer happens to be reviewing a given pull request that week.

Run both in parallel, and let the flag do the deciding

Don't cut over. Run old and new behind a flag, routing a slice of real traffic to the new path and comparing outputs against the old one on every request, before the new path serves anyone alone. This is the same discipline that already governs every other deploy on a team that takes rollback seriously: a change goes out dark, gets checked against reality, and only then gets trusted with real weight.

Divergence is the number that matters here, not uptime and not test pass rate. Every mismatch between old and new output on the same input is a characterization gap you missed, and it's cheaper to find in a comparison log than in a customer's inbox. Expect divergence in week one. Chase it to zero before expecting to chase it anywhere else, and expect it to take longer than the actual code migration did. The translation is the cheap part now. Agreement is the expensive part.

Delete the old path on a date

The step almost everyone skips.

A migration that reaches "the new path is live" without a scheduled deletion of the old one isn't finished. It's now two systems, both live, both needing to be kept correct, for however long "eventually" turns out to mean, which in practice is measured in years rather than sprints.

Set the date when the parallel run starts, not after it succeeds. Divergence at zero for a defined window, under real traffic, is the condition. The date is the consequence, not a suggestion, and it belongs on the same document as the rest of the plan.

None of this is exotic. It's the ordinary discipline of a flagged rollout, applied to something bigger than a single feature, with an agent doing the translation work nobody wanted to do by hand in the first place. The novelty was never in the mechanics. It's that construction got cheap enough to make the old caution look expensive, which means the only thing standing between you and moving this particular boring service is whether anyone bothered to write down what it actually does.

Questions this answers

How do you migrate a legacy service using AI coding agents?
Write characterization tests that pin down what the old service actually does before touching anything, cut the work into small checkable units, let an agent handle the mechanical translation, and run old and new side by side behind a flag until divergence hits zero.
What should a human, not an agent, decide during an agent-assisted migration?
The seams: where the boundary of each unit falls, which edge cases the characterization tests must cover, and when the old path is safe to delete. An agent can translate known behavior faithfully. It can't tell you which undocumented behavior is load-bearing.
How do you know when it's safe to delete the old code path?
When the parallel-run divergence rate has held at zero for a defined window under real production traffic, not synthetic tests, and a specific date has been set in advance for deletion rather than left open-ended.