lastminute.com logo

Technology

From Months to Weeks: The Smart Way to Let AI Do a Migration

antonino_gitto
antonino gitto
timothy_russo
timothy russo

How we migrated 137 modules to a new dependency-injection pattern with AI, turning a quarter-long slog into a few focused weeks by teaching the AI instead of delegating to it, working in small supervised steps, and letting the method compound.


Every large codebase eventually accumulates a debt it can’t pay off in a single sprint. Ours had a good one: dependency injection wired the old way, threaded through 137 modules, every folder in the app that exposes a public index.ts, spread across features, services, components, and core. Migrating all of it to our new pattern was the kind of task that, done by hand, disappears into a quarter and takes a piece of the team with it.

So we did it with AI. And the interesting part isn’t that we used AI, everyone is. It’s how. Because the first way we tried, the obvious way, failed within a day. What actually worked was almost the opposite of the hype: less autonomy, not more. Smaller steps, not bigger leaps. Teaching before delegating.

This is the story of turning months into weeks without ever losing control of the wheel, and the mindset that got us there.

The trap: “just point a swarm of agents at it”

When you have 137 near-identical chores, the temptation is irresistible: spin up a fleet of agents, hand each one a module, and come back to a finished migration. Parallelism! Scale! We tried exactly that.

It broke immediately.

Here’s why, and it’s the single most important lesson in this whole piece: code that hasn’t been migrated yet has too many shades. From a distance, 137 modules look identical. Up close, every one has its own quirks, a slightly different way it registered dependencies, an odd circular import, a service that reached into another module’s internals, a test that mocked something three layers deep. No single “generic” transformation survives contact with that variety. An instruction general enough to cover every case is too vague to be safe; an instruction precise enough to be safe only covers a handful of cases.

And AI is flaky in a very specific, very dangerous way for this kind of work. It’s confidently wrong at the margins. It will migrate a module beautifully, then invent a subtle change in the one edge case you weren’t watching. In a normal feature, a small wrong change is a bug you catch in review. In a migration, a small wrong change is an avalanche. It compounds: the next module builds on the broken assumption, the shared mock drifts, the require-cycle you thought you fixed reappears three modules downstream. By the time a test finally goes red, you can’t tell which of the last twenty changes started the slide.

Unsupervised parallelism doesn’t just risk this, it guarantees it, and then hides the trigger. We shut it down after a day.

The turn: teach the AI, don’t delegate to it

The reframe that saved the project was simple. Stop thinking of the AI as a workforce you dispatch. Start thinking of it as an apprentice you train.

So we did the first module ourselves. By hand. Slowly. Not because we couldn’t have muddled through with AI, but because we wanted a reference implementation, a gold-standard migration we fully understood, with every decision made deliberately. Then we turned to the AI and said, in effect: here is what good looks like; learn from it.

That reference became the seed for our tooling. Instead of prompts we retyped and hoped about, we captured the migration as reusable skills, small, named, single-purpose procedures, and specialized agents that knew how to apply them. One skill to migrate a single file. One to migrate a test alongside it. One to compose a module’s index.ts once its files were done. One to create the mock that keeps the test suite from crashing on native calls. Each did one thing, the way we’d done it by hand, and no more.

The AI wasn’t inventing the migration anymore. It was executing a method we’d authored, and that changed everything.

Reins, not autonomy: break the step into steps

The second breakthrough was discovering that we could keep subdividing.

Our instinct had been “migrate a module” as the unit of work. But a module migration is really a chain: migrate each file, fix each file’s test, compose the index, wire the mocks, reconcile the imports. When we handed the AI the whole chain at once, we got a big diff and a binary outcome, it worked, or it failed somewhere in the middle and we went spelunking.

When we split it into those inner steps and ran them one at a time, two things happened. First, control, we inspected the seam after each step, so a mistake surfaced at step three instead of hiding until step nine. Second, speed of reaction, a wrong turn was a five-minute correction to one skill, not an archaeology dig through an hour of accumulated changes. Counterintuitively, doing it in more, smaller steps made us faster, because we spent almost no time debugging avalanches. There were no avalanches to debug.

This is the heart of it: granularity is how you tame flakiness. You don’t make AI reliable by trusting it more. You make it reliable by giving it less room to be wrong between checkpoints.

Validate before you scale

We had a method and we had tooling, but we didn’t yet have proof. So the second module wasn’t about speed, it was a controlled experiment. We ran the skills and agents end to end and watched: did they reproduce the quality of our hand-done reference? Did the steps compose cleanly? Where did the AI hesitate or guess?

They held up. And only then did we let ourselves scale.

That order matters. It’s tempting, once something works twice, to declare victory and unleash it. But the second module is where you’re still cheap to be wrong. Validating there, deliberately, skeptically, bought us the confidence to move fast everywhere after, precisely because we’d earned it instead of assumed it.

Attack order: shrink the blast radius, module by module

The last piece was sequencing. We didn’t migrate modules in whatever order they came up. We went from fewest dependencies to most, the leaf modules first, the deeply-connected ones last.

The reason is blast radius. A module nobody depends on can be migrated in near-isolation; if something’s off, it’s contained. Migrate that one first and it becomes solid ground for the next. Each completed module reduced the number of un-migrated dependencies the following modules had to worry about, so the hardest, most-connected modules, the ones we’d have dreaded on day one, were the easiest by the time we reached them, because almost everything they touched had already been migrated and proven.

The graph shrank as we climbed it. And through all of it, the app kept building, tests kept passing, and the rest of the team kept shipping features on top. Zero downtime. Nobody had to stop and wait for “the migration” to finish, because the migration was never a single disruptive event, it was a steady, safe advance.

The compounding payoff

Here’s the part that felt like magic, though it wasn’t. Every module we finished made the next one cheaper.

Not just because the dependency graph was shrinking, but because every quirk we hit sharpened the tooling. A module did something the skill didn’t expect? We taught the skill that shade, once, and every subsequent module inherited the lesson. The method didn’t stay static and degrade, it improved as it ran. By the back half of the migration, the skills had absorbed enough of the codebase’s variety that the AI handled cases in seconds that would have stumped it in week one.

That’s the curve that turns months into weeks. Not raw AI horsepower, a compounding method, supervised by people who kept feeding it back what they learned.

What we’d tell you to take from this

If you’re staring at a migration or a mechanical refactor at scale and wondering whether AI can eat it, it can. But not the way the demos imply. Here’s the practice, distilled:

  1. Don’t go all-in. Autonomy is not the goal; a correct result is. Give AI the smallest possible room to be wrong between your checks.
  2. Teach before you delegate. Do the first one by hand, understand it completely, and make that the thing the AI learns from. A reference implementation beats a clever prompt.
  3. Split the work until the steps are boring. Small, single-purpose steps make mistakes surface early and cheap to fix. Granularity is your defense against the avalanche.
  4. Validate on the cheap cases before you trust the expensive ones. Prove the method on module two, not module fifty.
  5. Sequence to shrink the blast radius. Least-connected first. Every safe step makes the next one safer.
  6. Let the method compound. Feed every surprise back into the tooling. The system should get smarter as it runs, not sloppier.

The deepest shift is one of posture. It’s tempting to treat AI as an autopilot you switch on and walk away from. Treat it instead as an apprentice with real talent and no judgment, fast, tireless, occasionally and confidently wrong. Your job isn’t to do the work anymore. It’s to be the judgment: to teach, to check the seams, to sequence the approach, and to catch the small wrong thing before it becomes the big wrong thing.

Do that, and the quarter-long slog becomes a few focused weeks, with your architecture cleaner at the end than it’s ever been, and not a single day of downtime along the way.


About antonino gitto

antonino_gitto
Software Engineer

Antonino is an experienced mobile developer specializing in building high-quality, scalable apps. Passionate about innovation, he excels in creating seamless user experiences and solving complex challenges in mobile development.

About timothy russo

timothy_russo
Software Engineer

Timothy is a Mobile Product Engineer specialising in cross-platform mobile development. With a background in eCommerce, quality assurance, and project management, he is passionate about clean architecture, developer tooling, and building performant, scalable mobile experiences for iOS and Android.


Read next

The software engineer skills pyramid: how we encoded our own harness

The software engineer skills pyramid: how we encoded our own harness

fabrizio_duroni
fabrizio duroni
davide_botti
davide botti
timothy_russo
timothy russo

In this post we will show how we encoded our software development lifecycle in a set of custom skills and subagents. [...]

How We Enforce Architecture Boundaries at Scale on our App

How We Enforce Architecture Boundaries at Scale on our App

antonino_gitto
antonino gitto
timothy_russo
timothy russo

Learn how we automatically enforce architectural boundaries at scale in a large React Native codebase, with an incremental, opt-in approach that keeps teams unblocked while keeping the architecture clean. [...]