← Back

The new economics of refactoring code

· Filip Danisko

Large platform migrations used to be slow to delivery and because of that hard to justify.

I know because I spent a few years on platform teams.

Most of the time, the hard part was not knowing where the system should go. The hard part was getting it there without breaking everything on the way.

Everyone could see the issues.

The split repositories. Slow CI. The contract packages that had to be published before the frontend could consume them. The backend PR waiting on the frontend PR. The QA tests failing somewhere else, later, with nobody immediately sure whether the product broke or the test was just stale.

It's not that people didn't care.

They just had more urgent things to do.

That is what technical debt often is. Just a problem whose fix does not fit the current cost structure.

With AI, the cost structure changed.


The bottleneck moved

For years, the bottleneck was human implementation speed.

If a migration takes months, then every bit of friction around it gets absorbed into the size of the project.

Two PRs instead of one. A package release in the middle. A slower CI.

Annoying, yes.

But when the actual implementation work is already slow, those things feel like five percent of the pain.

Then LLM agents arrive and compress the implementation work.

And suddenly that five percent becomes painful.

When an agent can push through mechanical changes in hours, the expensive part is no longer only the code. It is the shape of the codebase around the code.


The new loop

The work became something like this.

Define the target state. Snapshot the current behavior. Give the agent constraints. Keep it inside a verification loop until the system is green again.

That sentence sounds too simple, but it describes the experience surprisingly well.

You are not asking the agent to become an architect. You still need to know where the system should go. You still need to decide what must remain true. You need to review the diffs and stop the agent when it starts fixing the wrong thing.

But once the target is clear, a lot of the painful middle disappears.

Broken imports. Circular dependencies. Type errors. Missing dependencies. Runtime failures that only appear after the previous layer has been fixed.

In the old model, that consumed a lot of human attention.

In the new model, an agent can sit in the loop for hours.

Change something. Run the check. Read the failure. Patch the next thing. Repeat.

That is not magic.

It is the same grind, but now the machine can stay in it longer than you can.


What we migrated

We recently went through this while moving our product monorepo.

Before the migration, we had a frontend monorepo, a backend monorepo, and a separate QA repository.

The backend exposed a contract package that had to be published and then consumed by the frontend. A feature that touched both sides of the product often required multiple pull requests, package releases, dependency bumps.

The worst part was not the ceremony itself. It was the delay in feedback.

A backend contract could be implemented against one understanding of the feature. Then the frontend would consume it later. By then, the product thinking might have shifted. Or the integration would reveal something that should have been obvious earlier, but could not be tested in one place.

QA had a similar problem.

Tests lived outside of the main product code, which meant failures often came with accidental detective work.

Did the product change? Did the test become stale? Was this a real regression?

That is not valuable engineering work.

It is synchronization tax.


One product-shaped workflow

After the migration, backend, frontend, and QA all live in the same product monorepo.

A feature PR can now change the backend, update the frontend, and run tests in one place.

The review surface is bigger, but the change is more honest. It shows the product moving as one system.

We also moved from npm to pnpm, aligned TypeScript versions, improved infrastructure setup, and cleaned up parts of the dependency graph.

The technologies mostly stayed the same.

The operating model changed.

That is the part I care about.

LLMs did not make monorepos good. Monorepos were useful before LLMs.

But agents make the value of a unified codebase much more obvious.

If the agent can operate across the whole product surface, and the repository can verify the result quickly, then large changes become much more practical.


Moving large pieces

This is the part that felt new.

Not writing code faster.

Moving large pieces of the codebase toward a target state.

I remember the old pain of merging packages together. Scripts, search and replace, and a good plan helped, but it was still a lot of work.

You move one piece, and five other assumptions break. You fix those, and the next layer appears.

With agents, that work felt different.

Before making large changes, I captured snapshots of the existing system. Build outputs. Dependency trees. Package relationships. The shape of generated artifacts. The checks that represented the current known-good state.

Then the work became iterative.

Typecheck. Lint. Build. CI. Runtime smoke tests. Artifact comparisons.

Each failing check gave us something concrete to fix.

That is where agents are useful: they can keep working through a tight loop.


Where agents fail

They fail in very predictable ways.

The agent wanted to edit lockfiles directly. That is almost always the wrong move. Lockfiles are generated artifacts. Inspect them, yes. But regenerate them through the package manager.

It also sometimes tried to make errors disappear by removing the code path that triggered them.

That can make TypeScript happy while breaking the application.

A green typecheck is not the same as a working system.

Another failure mode was local optimization. The agent would fix the immediate error in front of it, but create a deeper runtime problem later.

That is why the human role does not disappear.

Someone still has to define the target, set the constraints, and decide when a change is actually correct.

The agent handles more of the grind.


The codebase as an agent interface

This is where I think the work is going.

In the pre-LLM world, we often argued about rules as constraints on humans.

Do we really need this TypeScript rule?

Is this ESLint rule worth enforcing?

Is this package boundary too strict?

Those questions still matter. But they are no longer the only questions.

As agents touch more of the code, the question becomes different.

What rules are we imposing on agents?

Because the stricter the environment is, and the faster the feedback is, the more useful the agent becomes.

A loose codebase gives the agent too many ways to appear correct. It can make the typecheck pass while breaking runtime behavior. It can satisfy one package while creating a hidden dependency problem somewhere else. It can optimize locally and damage the system globally.

A strict codebase gives it fewer places to hide.

Types. Lints. Tests. Package boundaries. Runtime checks. Fast builds. Clear ownership.

Together, they make agent-driven changes easier to control.


Fast feedback matters more now

During the migration, I looked back at the agent runs and analyzed the tool calls.

Across the month, build commands alone took around eighteen hours of runtime.

That means the agent spent roughly eighteen hours just waiting for build results.

Not thinking. Not editing. Waiting.

That number changed how I think about build performance.

A slow build was always painful for humans. But humans context-switch. They get coffee. They answer Slack. They mentally amortize the delay.

Agents do not amortize it the same way.

If the agent is inside a tight verification loop, every slow check directly slows down the migration. Every unnecessary build, every uncached package, every broad task that could have been narrower becomes part of the cost of using the agent.

Make the builds faster, and CI gets faster.

Make CI faster, and developers get faster.

But now agents get faster too.

That changes the ROI of platform work.

Optimizing build time is no longer just about developer experience. It is about increasing the throughput of automated codebase transformation.


What actually changed

The technical debt has a different economic profile now.

A lot of debt was not impossible to fix. It was just too expensive to fix safely.

Too many files. Too much coordination. Too much repetitive debugging. Too much review overhead. Too much time spent getting back to green.

LLM agents make this type of work cheaper.

Large. Mechanical. Verifiable transformations.

Not every refactor fits this model. If the target is ambiguous, if correctness depends mostly on product judgment, or if you do not have a fast feedback loop, the agent will drift.

But when the work can be constrained, the economics change.

It no longer felt like something that required a dedicated platform team for months.

It felt like something a small number of engineers could push through by building the right verification loops and letting the agent grind through the mechanical parts.


The new bottleneck

Once implementation gets faster, old friction gets more expensive.

Two repositories, delayed package publishing, separate QA, slow CI, and implicit dependency boundaries all become more painful.

The old workflow was built around humans being the bottleneck.

But if agents are now doing more of the mechanical work, the repository, CI, and release process become part of the agent interface.

That is the shift.

Refactoring is no longer only about changing code.

It is about designing systems where large changes can be made, verified, and reviewed without losing control.

And once you experience that, old technical debt starts to look different.

Just much more movable than it used to be.