Managing Technical Debt: When to Refactor and When to Rewrite Code from Scratch

By nolimeo · March 6, 2026
banner image

Consider a situation that is the daily reality of many growing businesses: your e-commerce store, ERP system, or client portal runs on legacy, undocumented PHP code. The developers who wrote it five years ago are long gone. The current team is afraid to make any changes in the codebase because a modification in one module unexpectedly breaks three others on the opposite side of the application.

Every request for a new feature from marketing or sales takes weeks instead of days and is accompanied by endless hotfixes. Frustration rises on both sides—management sees no progress, and the technical leadership (CTO) faces constant pressure.

This is a classic manifestation of technical debt. Similar to financial debt, technical debt demands interest—and in this case, the interest is slowed innovation, high error rates, the departure of frustrated developers, and direct revenue losses.

At the nolimeo technology studio, we help mid-market and enterprise businesses pay off this debt safely. In this guide, we look at how to quantify the risks in legacy software through a professional technical audit and how to make a pragmatic decision between gradual refactoring and rewriting from scratch.


1. The Strangler Fig Pattern: Gradual Evolution Instead of Revolution

The biggest mistake when dealing with legacy software is attempting an immediate, full rewrite of the entire system while keeping it running. Such projects frequently end in disaster—they take twice as long, cost triple the budget, and by the time they launch after two years, the market has moved on, rendering the original specifications obsolete.

At nolimeo, we advocate a modern architectural approach known as the Strangler Fig Pattern. Instead of destroying the legacy system, we build a new headless infrastructure around it and gradually, module by module, migrate business logic to the new codebase.

       1. INITIAL STATE                 2. TRANSITION STATE               3. TARGET STATE
                                      ┌──────────────────┐
                                      │  Reverse Proxy   │
                                      └──────────────────┘
                                       /                \
                                      /                  \
    ┌──────────────────┐      ┌───────────────┐  ┌───────────────┐      ┌──────────────────┐
    │                  │      │  New Module   │  │Legacy Monolith│      │                  │
    │ Legacy Monolith  │      │(Next.js API)  │  │ (Legacy PHP)  │      │   New Headless   │
    │   (Legacy PHP)   │      └───────────────┘  └───────────────┘      │ System (Next.js) │
    │                  │              │                  │              │                  │
    └──────────────────┘              ▼                  ▼              └──────────────────┘
                              ┌──────────────────────────────────┐
                              │         Shared Database          │
                              └──────────────────────────────────┘

How the Strangler Pattern works in practice:

  1. Introducing a Proxy Gateway: We place an intelligent reverse proxy or Next.js API router in front of the legacy monolith.
  2. Migrating the Most Critical Module: We identify a key area (such as product search or user registration). We rewrite this module in a modern, type-safe environment (like Next.js and Medusa.js).
  3. Routing Requests: The proxy server automatically begins routing search traffic to the new system, while the rest of the site (orders, invoicing) is still handled by the legacy PHP application.
  4. Gradual Decommissioning: Step by step, we migrate the remaining modules until the legacy monolith is completely decommissioned—"strangled" by the new architecture.

2. Technical Implementation: Next.js Strangler API Router

To implement this pattern in Next.js, we can utilize a dynamic catch-all API route that acts as an intelligent router. It forwards legacy API endpoints to the old server while serving refactored, optimized requests from a new microservice.

Below is a TypeScript example of our production-grade middleware that asynchronously filters and routes requests based on a migrated routes list:

// src/app/api/[[...path]]/route.ts
import { NextRequest, NextResponse } from "next/server";

const LEGACY_BACKEND_URL = process.env.LEGACY_BACKEND_URL || "https://legacy-monolith.company.com";
const NEW_API_URL = process.env.NEW_API_URL || "https://api-v2.company.com";

// List of API paths that have been successfully migrated to modern code
const MIGRATED_PATHS = [
  "/api/v1/products/search",
  "/api/v1/cart/add",
  "/api/v1/checkout/validate-vat"
];

/**
 * Intelligent Asynchronous Router for Request Direction (Strangler Pattern)
 */
export async function GET(req: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) {
  const resolvedParams = await params;
  const path = "/" + (resolvedParams.path?.join("/") || "");
  const searchParams = req.nextUrl.search;
  
  // Determine if the requested path matches a migrated module
  const isMigrated = MIGRATED_PATHS.some(migratedPath => path.startsWith(migratedPath));
  const targetBaseUrl = isMigrated ? NEW_API_URL : LEGACY_BACKEND_URL;
  const targetUrl = `${targetBaseUrl}${path}${searchParams}`;

  try {
    const headers = new Headers(req.headers);
    // Set X-Forwarded-Host to preserve original domain context on the target server
    headers.set("X-Forwarded-Host", req.headers.get("host") || "");

    const response = await fetch(targetUrl, {
      method: "GET",
      headers: headers,
    });

    const data = await response.text();

    return new NextResponse(data, {
      status: response.status,
      headers: response.headers,
    });
  } catch (error: any) {
    console.error(`Routing error for path ${path}:`, error.message);
    return NextResponse.json(
      { error: "Routing gateway temporarily unavailable", detail: error.message },
      { status: 502 }
    );
  }
}

Why this approach is effective:

  • Minimal Downtime: Users and Google crawlers see the same domain and identical API endpoints while the software change happens in the background.
  • Rapid Fault Mitigation: If a newly rewritten endpoint fails, we can quickly remove it from the MIGRATED_PATHS array on the router. Traffic routes back to the verified legacy PHP application without client-side interruption.

3. Decision Matrix: Refactoring vs. Rewriting from Scratch

As a CTO or business owner, your technical strategy must be driven by data rather than gut feelings. Here is our pragmatic matrix for assessing legacy code:

Evaluation Metric Choose Refactoring (Strangler Evolution) Choose Complete Rewrite (From Scratch)
Core Database Architecture The code is cluttered, but the database schema and relational structures are fundamentally sound. The database design is so flawed that adding features requires complex, fragile workarounds.
Language & Framework Health The current language version and framework still receive security patches. The application relies on deprecated dependencies or legacy PHP versions (pre-7.4) that are EOL.
Documentation & Business Logic There is a clear operational understanding of the business logic and core integration points. No one in the company understands how pricing matrices work, and the code lacks documentation or tests.
Timeline & Budget Constraints You must continuously deliver value and cannot pause feature development. The business can allocate budget for a parallel team and accept a 6-12 month freeze on new features.

4. How nolimeo's Technical Audit Protects Your Investment

Deciding the future of your company's software should never be a guessing game. That is why our team at nolimeo begins every engagement with a comprehensive Software Health and Technical Audit.

Our senior engineers audit your repositories across four dimensions:

  1. Static Code Analysis: We measure cognitive complexity, code duplication, and test coverage using industry-standard tools.
  2. Security Hygiene: We identify vulnerabilities in third-party libraries (CVEs) and detect data transport risks.
  3. Performance Bottlenecks: We locate unoptimized database queries (such as the N+1 query problem) that slow down page loads.
  4. Business Risk Mapping: We translate technical debt findings (like lack of type-safety) into business consequences (such as annual bug-fixing costs of $50,000).

The output of our audit is not a generic, theoretical slide deck. Instead, you receive a concrete, actionable plan outlining budget options and phases, showing exactly where to start refactoring and how to migrate to a stable headless architecture without risking business stability.


Conclusion: Stop Paying Interest on Your Technical Debt

Technical debt is a silent blocker of business agility. While your team spends months debugging legacy monoliths and applying quick fixes, competitors using modern headless stacks roll out campaigns in hours and enjoy fast loading speeds.

At the nolimeo technology studio, we build clean, resilient software. As a premium boutique studio, we do not deploy template wrappers or assign projects to junior staff. Our clients collaborate directly with senior engineers, receiving robust architectures backed by long-term support.

Ready to gain more control over your software assets, reduce recurring downtime, and decide whether to refactor or rewrite? Contact us and we’ll review your repositories, risks, and the safest modernization path.

Interested in pushing your project forward?