Continuous DeliveryApr 27, 2026

Feature Flags and the Strangler Fig: Refactor Legacy Code Without the Big-Bang Rewrite

M
Marcus Johnson
Staff Engineer

Every team has one: the module nobody wants to touch. It works, mostly. The tests are sparse. The original author left two years ago. And every quarter, somebody pitches the rewrite.

Six months later, the rewrite is half-finished, the old code is still in production, and you've shipped exactly zero features from that team. There is a better way.

Why Big-Bang Rewrites Fail

Legacy code is dense with edge cases nobody documented. The quirks aren't bugs — they're the accumulated answers to support tickets, regulatory questions, and customer requests that shaped the system over years. A clean-room rewrite throws all of that away and rediscovers it the painful way: in production.

The cutover compounds the risk. You build the new system in parallel, freeze the old one for a weekend, run the migration, and pray. If the new system has gaps, you're debugging on a live customer base with no fallback.

The Strangler Fig Pattern

Martin Fowler named the strangler fig after the tree that grows around its host until the host eventually rots away. Applied to software: build the new code path alongside the old, route a small slice of traffic to the new path, and grow that slice over weeks or months. When the new path handles 100% of traffic, you delete the old code.

The pattern only works if you can flip between paths instantly, per request, without redeploying. That's where feature flags earn their keep.

import featureflow from 'featureflow-client';

const client = featureflow.init(process.env.FEATUREFLOW_API_KEY!, {
  user: {
    key: userId,
    attributes: { plan: user.plan, region: user.region },
  },
});

export async function calculateInvoice(orderId: string) {
  const useNewEngine = client.evaluate('billing-engine-v2').isOn();

  if (useNewEngine) {
    return billingV2.calculate(orderId);   // new strangler path
  }

  return billingV1.calculate(orderId);     // legacy host
}

One conditional. Two implementations. The flag controls the ratio. Start at 1% of internal users, watch error rates and invoice totals, then ramp.

Make the Migration Boring

Run both paths in shadow first. Before you route a single user to the new code, call both implementations in parallel and compare outputs. Log the diffs. You'll find the undocumented edge cases before any customer does.

Slice by surface area, not by time. Don't aim for "rewrite finished by Q3". Aim for "route /api/invoices through v2 for plan=enterprise by next Friday". Each slice ships independently and earns confidence for the next one.

Keep the kill switch close. When billingV2 mis-rounds a currency conversion at 3am, you flip the flag off from the dashboard. The next request goes through the legacy path. You debug in the morning, not at 3am.

Delete the old code on a deadline. The strangler fig only works if the host actually dies. Add a ticket the day you reach 100% rollout: remove billingV1, remove the flag, remove the conditional. Otherwise the dual-path stays forever and you've added complexity without removing any.

Where This Fits

The pattern scales from a single function to a service boundary. Replacing an in-process module? Wrap the call site in a flag. Splitting a monolith into a service? Route a percentage of requests through the new service via the same mechanism. Migrating from one third-party API to another? Same shape.

Featureflow gives you the targeting, percentage rollouts, and instant kill switches that make the strangler fig safe in production. See featureflow.com or browse the docs to see how the SDK fits into your refactor.

#FeatureFlags#StranglerFig#Refactoring#LegacyCode#ContinuousDelivery#DevOps

Replace legacy code without the big-bang weekend

Start free with Featureflow — gradual rollouts and instant kill switches built for safe refactors.

Start Now (Free)

Related Articles