Expanding refactor

Published:

The plan is to untangle the report builder.

Two export formats use slightly different versions of the same calculation. The team agrees to bring that calculation into one place, preserve the existing output, and make the next report change easier to implement.

By Tuesday, the engineer has found a problem with the objects passed into the builder. Fixing those objects would make the extraction cleaner. On Wednesday, that change reaches the persistence layer. By Friday, the branch includes a new repository interface, a replacement configuration mechanism, and the beginning of a migration for every report type.

The original duplication is still there. Removing it now depends on finishing the new structure.

This is an expanding refactor: a planned improvement to a bounded area of code grows into a much broader reconstruction. Each discovery supplies a reason to widen the work. The point at which the team will receive a usable improvement keeps moving away.

The pattern begins with a technical intention. Someone wants to improve or revise existing code, and that effort gradually becomes a rewrite. Product expectations can remain exactly as they were while the implementation acquires a much larger ambition.

The expansion often follows real dependencies.

An awkward interface forces callers to know too much. A test fixture requires half the application to start. Two modules depend on each other in ways the diagram never showed. The engineer discovers that the proposed boundary exists in conversation but has little support in the code.

Working through those discoveries can be valuable. An estimate made before opening the code may have missed essential work. A deeper repair may offer a better outcome than preserving a fragile design through another round of patches.

The difficulty begins when every discovery becomes a prerequisite for the current task.

An engineer reasons that the extraction will be easier after the objects change. The objects will be easier to change after persistence is separated. Persistence will be easier to separate after configuration is replaced. Each step makes sense beside the previous one. Together, they move the team far beyond the decision that funded the work.

The explanation becomes a chain of things that must happen first.

Look for that chain in updates and review conversations. “Once this is in place” starts appearing more often than an account of what is already usable. The branch touches areas whose connection to the original purpose takes several steps to explain. Tests increasingly describe a future structure while the current system waits for the migration that will make it complete.

The growing change also recruits more owners. A local improvement begins to require agreement from the engineers responsible for storage, configuration, deployment, and another service. Their questions expose further consequences. The author spends more time reconciling the replacement with the existing system, and the initial repair becomes a small part of the discussion.

Repository history can help locate the turn.

Compare the original proposal with the latest diff. Which responsibilities moved? Which interfaces changed? How many callers now need migration? Did a second implementation appear beside the first? Can any completed part be reviewed, verified, and used independently?

File counts and lines changed offer clues. A mechanical update may touch hundreds of files while preserving a straightforward purpose. A small change to a shared interface can commit several teams to weeks of work. Read the dependencies created by the change alongside its size.

The useful signal is a widening obligation: more of the system must change before the team can accept the result.

That gives this pattern a different shape from code going in circles. There, an engineer repeatedly revises a narrow area while useful learning fades. An expanding refactor can keep producing meaningful discoveries and technically sound code. Its difficulty lies in how those discoveries enlarge the project and delay a usable result.

Clean-as-you-go work has a local stopping point. A misleading name is corrected, an obsolete branch disappears, and the intended change can proceed. In an expanding refactor, improvement repeatedly opens another area that must be repaired before the current work can finish.

Several conditions make that movement attractive.

The engineer may have spent months working around the same design and finally received time to improve it. They know that deferred maintenance rarely returns to the top of the plan. The present task feels like the only opportunity to make the repair properly, so every related weakness joins it.

A vague assignment can create the same result. “Clean up reporting” gives the engineer responsibility for an area without defining which difficulty the investment should remove. Every imperfection belongs to reporting. Completion becomes a matter of personal satisfaction, and the code always supplies another reason to continue.

Sometimes the proposed replacement becomes easier to imagine than the path through the existing system. A coherent new design promises relief from years of accumulated exceptions. Migration exposes those exceptions one by one. Each carries behavior someone still depends on, even when its purpose was poorly recorded.

The team can reinforce the expansion through review. One reviewer requests a more general interface. Another asks for consistency across all callers. A third wants an older abstraction removed before the new one arrives. Each comment can be sensible while the collection silently commissions a larger project.

By then, stopping feels expensive. The branch contains substantial work. The engineer has invested care and judgment. Returning to a smaller repair may feel like throwing that effort away, and every additional day makes the larger design feel closer to inevitable.

A useful intervention starts by reconstructing the decision.

“We planned to share the report calculation. The branch now replaces storage and configuration as well. What did we learn that connects those changes?” gives the engineer room to explain the dependencies. Ask them to show a concrete case where the smaller approach fails or leaves an unacceptable cost.

Bring in a colleague who understands the affected area. Together, separate the changes required to reach the original outcome from improvements that would make the wider system preferable. Some dependencies will prove essential. Others will become options once someone questions the order in which the work must happen.

Then compare plausible paths from the code that exists today.

The team might complete the original extraction through the current interfaces, deliver a smaller structural improvement first, or fund the broader replacement explicitly. It might also stop the effort if the expected benefit has weakened. For each path, discuss the remaining work, the uncertainty, the behavior at risk, and the point at which someone can use the result.

Past effort helps explain the current state. The decision turns on what each available path will cost and provide from here.

In the reporting example, a narrow interface around the shared calculation may let both exporters use it without replacing persistence. The awkward objects remain visible follow-up work. If the existing object model genuinely prevents correct extraction, the team may first separate one responsibility and migrate one exporter to prove the approach.

That first step needs a complete route into use. A new interface sitting unused beside the old one adds maintenance before it supplies relief. A useful increment includes its caller, tests, deployment consequences, and any old path it can safely retire.

Define completion in terms the team can verify.

For this change, both export formats use the same calculation, existing output remains stable, and the duplicate implementation is removed. Other report types, configuration changes, and storage redesign stay outside the current effort. If new evidence makes one of those necessary, the team returns to the decision before extending the branch.

For a broader replacement, completion also includes migration and removal. Name which consumers will move, how their behavior will be checked, and who will maintain any period of coexistence. A clean replacement with an unfinished migration leaves the team carrying both systems.

The people responsible for technical direction should assess the design and its risks. The people responsible for delivery need to understand what the investment displaces. A wider refactor can be the right choice, and that choice needs a plan the affected people actually share.

Managers can make these conversations easier by giving maintenance a credible place in planning.

A follow-up record should preserve the observed cost: adding a report format currently requires changing three unrelated storage classes, for example. Include the next useful investigation or repair. Return to that evidence when choosing technical work. Engineers have more reason to respect a boundary when discoveries have a practical route into future decisions.

During the work, agree on a point to inspect the approach before it spreads. Review one migrated caller. Check the first extracted responsibility against the next likely use. If that step exposes a wider dependency, discuss it while several options remain affordable.

The review should leave the team able to explain what will finish next and what can remain as it is. Repeated inability to name that boundary deserves attention even when every individual change looks good.

Back in the report builder, the team chooses the smaller extraction. The engineer keeps the tests that captured the export behavior, moves the shared calculation, and records why the object model made the work difficult. The storage redesign becomes a separate proposal with evidence and an explicit cost.

The improvement reaches production. Both exporters use it. The next report change can begin.