Skip to main content
INS // Insights

Legacy Codebase Modernization: The Strangler Fig Approach

Updated June 2026 · 7 min read

Legacy codebase modernization is almost never the dramatic big-bang rewrite that teams fantasize about. Rewrites take 3-5× longer than estimated, lose critical domain knowledge encoded in existing behavior, and deliver the same architectural mistakes in a new language. The approach that works — the strangler fig pattern — incrementally replaces legacy components while keeping the existing system running.

Named after a fig species that grows around a host tree, gradually replacing it, the strangler fig pattern allows you to modernize incrementally, validate at each step, and abandon the effort cleanly if priorities change.

Why Rewrites Fail

Before covering the right approach, understand why rewrites fail — so you don't repeat the pattern:

Discovery scope creep: The legacy system does things nobody remembers. Edge cases, error handling paths, integration behaviors that were added over years of production operation. Rewrites discover these requirements mid-way through, extending timelines dramatically.

The existing system keeps evolving: While the rewrite team is building V2, the product team keeps shipping features on V1. The rewrite chases a moving target.

Parallel maintenance tax: Maintaining two systems simultaneously doubles operational overhead and dilutes engineering focus.

The illusion of the blank slate: Given a blank slate, teams make many of the same architectural decisions the original team made — because they're responding to the same product requirements, team constraints, and deadlines. Rewrites often produce a shinier legacy system.

The Strangler Fig Pattern in Practice

The strangler fig pattern works by: 1. Identifying a discrete subsystem that can be extracted 2. Building the new implementation alongside (not replacing) the old one 3. Routing a small percentage of traffic to the new implementation 4. Gradually shifting traffic as confidence increases 5. Deleting the old implementation when traffic is fully migrated

[Existing Monolith]
       ↓
[Routing Facade / API Gateway]
  ├── Legacy path (existing code)
  └── New service (new implementation)
       ↓ (gradually: 5% → 25% → 50% → 100%)
[New Service]

The routing facade is the key infrastructure piece. It can be a feature flag, an API gateway routing rule, or an application-level router — the important thing is that it allows gradual traffic shifting with easy rollback.

Phase 1: Domain Boundary Identification

Before extracting anything, map your domain boundaries. Good extraction targets: - High cohesion: The subsystem has clear, well-defined responsibilities - Low coupling: It communicates with the rest of the system through well-defined interfaces - Independent deployability: Changes to this subsystem shouldn't require coordinated changes elsewhere - Business value alignment: The subsystem maps to a discrete business capability

In a typical legacy monolith, good first extraction targets: - Authentication and authorization - User profile management - Notification delivery - Reporting and analytics - Third-party integrations

Poor first extraction targets: - Core transactional logic with widespread coupling - Database-heavy components with implicit schema dependencies - Components with no clear API boundary

Phase 2: API Contract Definition

Before writing any new code, define the API contract between the strangler facade and the new service. This contract is the commitment that allows old and new to coexist:

# OpenAPI spec for extracted notification service
openapi: 3.0.0
info:
  title: Notification Service
  version: 1.0.0

paths:
  /notifications/send:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [user_id, type, payload]
              properties:
                user_id:
                  type: string
                type:
                  type: string
                  enum: [email, sms, push]
                payload:
                  type: object
      responses:
        '202':
          description: Notification queued successfully

The legacy code and new service both implement this contract — ensuring the routing facade can send requests to either without modification.

Phase 3: Shadow Mode Deployment

Before migrating any real traffic, run the new implementation in shadow mode: - Legacy implementation handles all requests - New implementation receives the same requests simultaneously - Responses from both are compared; divergences are logged - No customer impact from new implementation errors

import asyncio
import logging

logger = logging.getLogger(__name__)

class ShadowRouter:
    def __init__(self, legacy_handler, new_handler):
        self.legacy = legacy_handler
        self.new = new_handler

    async def route(self, request):
        # Always use legacy for response
        legacy_response = await self.legacy.handle(request)

        # Shadow the request to new service without blocking
        asyncio.create_task(self._shadow_request(request, legacy_response))

        return legacy_response

    async def _shadow_request(self, request, legacy_response):
        try:
            new_response = await self.new.handle(request)

            if not self._responses_match(legacy_response, new_response):
                logger.warning("shadow_divergence", extra={
                    "endpoint": request.path,
                    "legacy": legacy_response,
                    "new": new_response,
                })
        except Exception as e:
            logger.error("shadow_error", extra={"error": str(e), "endpoint": request.path})

Run shadow mode for 1-2 weeks and fix all divergences. When divergence rate is near zero, you're ready for traffic migration.

Phase 4: Gradual Traffic Migration

Shift traffic in stages, monitoring error rates and latency at each step:

  • 5% → new service (observe for 24 hours)
  • 25% → new service (observe for 48 hours)
  • 50% → new service (observe for 48 hours)
  • 100% → new service (keep legacy on standby for 2 weeks)
  • Delete legacy code

Use feature flags (LaunchDarkly, Unleash, or a simple environment variable) to control routing percentage with instant rollback capability.

Maintaining Product Velocity During Modernization

The most common failure mode: modernization work competes with feature development and loses. Prevention:

Dedicate a portion of sprint capacity to modernization: A consistent 20-30% allocation (not subject to feature priority overrides) allows steady progress without blocking product development.

Modernize opportunistically: When a feature requires changes to a legacy component that's on the extraction roadmap, accelerate that component's extraction as part of the feature work.

Don't run two parallel product backlogs: New features go into the new service (not the legacy one) once the extraction is underway. This creates natural pressure to complete the extraction.

Rutagon Executes Legacy Modernization Engagements

Rutagon provides fractional engineering leadership and implementation for companies modernizing legacy systems without stopping feature delivery. Contact us at rutagon.com/contact.

See also: technical debt assessment and software engineering capabilities.

FAQ

How do you decide whether to modernize vs. rewrite vs. replace with a commercial product?

Modernize when: the existing system has valuable domain logic and data, and the core problems are architecture and maintainability rather than fundamental design. Rewrite when: the existing system is genuinely unsalvageable (wrong language, no documentation, completely untestable) and the scope is small enough to complete. Replace with commercial product when: the feature is non-core and a commercial solution covers 80%+ of your needs. The answer changes based on team size, business urgency, and how core the system is to your competitive differentiation.

How do you handle data migration during legacy modernization?

Data migration is usually the hardest part. Options: dual-write (write to both old and new data stores during transition), event sourcing to replay historical events into the new schema, or batch migration during a maintenance window. Dual-write is the safest for live systems — both stores stay in sync during the transition, and you can validate the new store's data against the old before switching reads over.

Can the strangler fig pattern work for a monolithic database, not just code?

Yes, but it's harder. Database strangler fig (sometimes called "expand-contract") works by: adding new tables/columns alongside old ones, updating writes to populate both, running both read paths in parallel and comparing results, then migrating reads to new schema and eventually dropping old schema. Tools like Flyway and Liquibase help manage the migration sequences. Database modernization typically takes 2-3× longer than service extraction.

What metrics indicate that legacy modernization is going well?

Deployment frequency of the new service vs. legacy (new should be higher — that's the whole point). Error rate of new service vs. legacy (should be lower or equal). Time to add new features (should decrease as extraction progresses). Proportion of codebase in legacy vs. modern (should shift over time). Developer experience metrics — are engineers spending less time on workarounds? Progress should be measurable, not just felt.

How do you handle the team dynamics when some engineers want to rewrite and others want to modernize?

Acknowledge the rewrite impulse as valid — the desire often comes from real frustration with a codebase that's genuinely difficult to work in. Make the tradeoffs explicit: show the typical rewrite timeline and success rate vs. the strangler fig pattern. If possible, start with a small extraction as a proof of concept before committing to either approach at scale. Engineers who are skeptical of incremental approaches often become believers after seeing a successful extraction delivered on time.

Ready to discuss your project?

We deliver production-grade software for government, defense, and commercial clients. Let's talk about what you need.

Initiate Contact