All insights

Technical strategy

Refactor or rewrite? A decision framework for aging products

How to distinguish frustrating code from a genuinely exhausted architecture—and improve a live system without gambling the business on a clean slate.

By QubixTeam Engineering··6 min read

Measure the constraint, not the frustration

Teams often ask for a rewrite after a painful release, but pain can come from missing tests, poor deployment practices, unclear ownership, or a handful of high-change modules. Replacing the language or framework does not automatically fix those conditions.

Collect evidence: lead time for common changes, defect concentration, recovery time, infrastructure cost, onboarding time, and parts of the system nobody can safely modify. The rewrite case should name the constraint it removes and how that improvement will be measured.

Try to buy safety first

Before replacing working behavior, add visibility around it. Characterization tests capture what the current system actually does, observability reveals the expensive paths, and a repeatable deployment process reduces the cost of every later change.

Look for seams where one capability can be isolated behind an API or module boundary. Moving a high-change area incrementally can create value sooner and test the proposed architecture under production conditions.

  • Map dependencies and data ownership.
  • Identify business rules that exist only in code or staff memory.
  • Separate operational problems from architectural ones.
  • Estimate migration, dual-running, training, and rollback—not only new development.

Know when replacement is justified

A rewrite becomes more reasonable when the current platform blocks required security support, cannot meet a demonstrated scale or reliability need, depends on unavailable expertise, or makes a necessary product model impossible without pervasive change.

Even then, a staged replacement is usually safer than a launch-day switch. Route a narrow capability to the new system, reconcile results, and expand only after its behavior is understood.

Plan for the data, not just the code

Data migration is often the real project. Define field mappings, invalid legacy cases, reconciliation reports, cutover ownership, and a rollback window. If old and new systems run together, decide which one is authoritative for every record and how conflicts are resolved.

Evidence from our work

Replacing an ad provider without carrying its hidden dependencies

Nitro VPN moved from an AdMob-oriented configuration to direct Unity Ads. The visible identifiers changed, but a Google consent dependency remained in the application lifecycle. That stale dependency disabled the new provider. The defect illustrates why replacement work needs a dependency inventory covering initialization, privacy, administration fields, remote configuration, UI state, documentation, and build dependencies.

The repair removed the obsolete dependency, applied Unity privacy metadata, kept server-side enablement, and added load callbacks. This was a contained refactor because the VPN connection and subscription models did not need replacement.

Limit: A contained provider migration can still require new privacy disclosures and store declarations. Code scope is not the same as compliance scope.