Strangler Fig Pattern — Safe Legacy Modernization with YARP and .NET

Posted on: 4/26/2026 6:11:59 AM

70%Big Bang Rewrites fail or miss deadlines
2–5 yrsAverage enterprise modernization timeline
60%IT budget spent on legacy maintenance
0%Downtime required when done right

You have a monolithic ASP.NET Framework 4.8 system that has been running in production for 10 years, millions of lines of code, hundreds of stored procedures — and management wants it "migrated to .NET 10 in 6 months." If the only approach you can think of is a complete rewrite (Big Bang Rewrite), stop right there. There is a far safer alternative: the Strangler Fig Pattern.

Named by Martin Fowler in 2004, this pattern draws inspiration from the strangler fig vine found in tropical forests — a plant that wraps around a host tree, gradually growing until it completely replaces the original. Applied to software: you build the new system in parallel, incrementally shifting traffic until the old system is "strangled" and can be shut down.

1. Why Big Bang Rewrites Are a Dangerous Gamble

Before diving into the Strangler Fig, let's understand why the "rewrite from scratch" approach so often fails:

CriteriaBig Bang RewriteStrangler Fig Pattern
Time to valueValue only after 100% completionDelivers value from the first migrated service
RiskExtremely high — 1 critical bug = rollback everythingLow — per-component rollback, no system-wide impact
Team loadMust maintain 2 systems in parallel long-termGradual transition, legacy scope shrinks over time
Business continuityFeature freeze during rewriteCan develop new features on migrated components
Success rate~30% finish on time and budgetSignificantly higher thanks to incremental delivery

Real-World Lesson

Netscape Navigator is the classic case study of a failed Big Bang Rewrite. They spent 3 years rewriting the browser from scratch while Internet Explorer captured the entire market. The result: Netscape lost its position and never recovered.

2. Strangler Fig Pattern: How It Works

The pattern operates on a 3-step cycle that repeats for each component:

graph TD
    A["🔍 Transform
Build new module to replace
legacy functionality"] --> B["🔀 Coexist
Run old + new in parallel
via routing facade"] B --> C["✂️ Eliminate
Shut down legacy module
once new is stable"] C --> A style A fill:#e94560,stroke:#fff,color:#fff style B fill:#2c3e50,stroke:#fff,color:#fff style C fill:#4CAF50,stroke:#fff,color:#fff

The Transform → Coexist → Eliminate cycle of the Strangler Fig Pattern

2.1. Routing Facade — The Orchestration Brain

The facade is the most critical component, acting as a proxy that routes requests to either the legacy or new system. In the .NET ecosystem, YARP (Yet Another Reverse Proxy) is the top choice for this role.

graph LR
    Client["Client
Browser/Mobile"] --> YARP["YARP Facade
.NET 10"] YARP -->|"/api/orders/*"| New["New Service
ASP.NET Core 10"] YARP -->|"/api/products/*"| New YARP -->|"/api/legacy/*"| Old["Legacy App
ASP.NET Framework 4.8"] YARP -->|"/reports/*"| Old style Client fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style YARP fill:#e94560,stroke:#fff,color:#fff style New fill:#4CAF50,stroke:#fff,color:#fff style Old fill:#ff9800,stroke:#fff,color:#fff

YARP acts as a Facade, routing requests based on route patterns

2.2. Anti-Corruption Layer (ACL)

The ACL is a protective shield that prevents the new system from being contaminated by the legacy's outdated domain model. It translates data between the two worlds, ensuring the new system maintains a clean design.

graph LR
    subgraph New_System["New System"]
        NS["Order Service
Clean Domain Model"] end subgraph ACL["Anti-Corruption Layer"] TR["Translator
+ Adapter"] end subgraph Legacy["Legacy System"] LS["tblOrder_Master
+ sp_GetOrderV3_Final2"] end NS --> TR TR --> LS style New_System fill:#e8f5e9,stroke:#4CAF50,color:#2c3e50 style ACL fill:#fff3e0,stroke:#ff9800,color:#2c3e50 style Legacy fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50 style NS fill:#4CAF50,stroke:#fff,color:#fff style TR fill:#ff9800,stroke:#fff,color:#fff style LS fill:#f8f9fa,stroke:#e94560,color:#2c3e50

The ACL prevents the legacy domain model from leaking into the new system

3. Practical Implementation with .NET and YARP

3.1. Setting Up YARP Reverse Proxy

The first step is configuring YARP as a facade sitting in front of both the legacy app and new services:

// Program.cs — YARP Facade
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

var app = builder.Build();
app.MapReverseProxy();
app.Run();
// appsettings.json — Route configuration
{
  "ReverseProxy": {
    "Routes": {
      "orders-route": {
        "ClusterId": "new-services",
        "Match": { "Path": "/api/orders/{**catch-all}" }
      },
      "products-route": {
        "ClusterId": "new-services",
        "Match": { "Path": "/api/products/{**catch-all}" }
      },
      "legacy-fallback": {
        "ClusterId": "legacy-app",
        "Match": { "Path": "/{**catch-all}" },
        "Order": 9999
      }
    },
    "Clusters": {
      "new-services": {
        "Destinations": {
          "primary": { "Address": "https://new-api.internal:5001" }
        }
      },
      "legacy-app": {
        "Destinations": {
          "primary": { "Address": "https://legacy.internal:8080" }
        }
      }
    }
  }
}

Practical Tip

Set Order: 9999 on the legacy-fallback route so it only matches when no other route fits. As you migrate more modules, just add new routes with higher priority — the legacy route automatically narrows in scope.

3.2. Feature Toggles for Gradual Rollout

Combining YARP with feature toggles enables percentage-based traffic shifting to minimize risk:

// Custom YARP middleware for feature-based routing
public class StranglerRoutingMiddleware
{
    public async Task InvokeAsync(HttpContext context)
    {
        var feature = context.Request.Path.StartsWithSegments("/api/orders");
        var useNewService = await _featureManager
            .IsEnabledAsync("OrderService.UseNew");

        if (feature && useNewService)
        {
            context.Items["__ClusterId"] = "new-services";
        }
        else
        {
            context.Items["__ClusterId"] = "legacy-app";
        }

        await _next(context);
    }
}

3.3. Anti-Corruption Layer in C#

// ACL: Translate legacy data model to clean domain
public class OrderAntiCorruptionLayer : IOrderRepository
{
    private readonly LegacyDbContext _legacyDb;

    public async Task<Order> GetByIdAsync(Guid orderId)
    {
        // Legacy uses int ID + stored procedure
        var legacyOrder = await _legacyDb
            .Set<TblOrderMaster>()
            .Include(o => o.TblOrderDetails)
            .FirstOrDefaultAsync(o => o.OrderGuid == orderId);

        if (legacyOrder == null) return null;

        // Translate to clean domain model
        return new Order
        {
            Id = orderId,
            CustomerId = CustomerId.From(legacyOrder.CustID),
            Status = MapLegacyStatus(legacyOrder.OrdStatus),
            Items = legacyOrder.TblOrderDetails
                .Select(d => new OrderItem
                {
                    ProductId = ProductId.From(d.ProdID),
                    Quantity = d.Qty,
                    UnitPrice = Money.From(d.Price, d.CurrCode)
                }).ToList(),
            CreatedAt = legacyOrder.CreateDT
        };
    }

    private static OrderStatus MapLegacyStatus(string status) =>
        status switch
        {
            "A" => OrderStatus.Active,
            "P" => OrderStatus.Pending,
            "X" => OrderStatus.Cancelled,
            "S" => OrderStatus.Shipped,
            _ => OrderStatus.Unknown
        };
}

4. Data Migration Strategy

This is the hardest part of the entire process. When extracting a service from the monolith, data must also be separated — following the Database per Service principle.

graph TD
    subgraph Phase1["Phase 1: Shared Database"]
        A1["New Service"] --> DB1["Legacy DB
Shared Access"] A2["Legacy App"] --> DB1 end subgraph Phase2["Phase 2: CDC Sync"] B1["New Service"] --> DB2["New DB"] B2["Legacy App"] --> DB3["Legacy DB"] DB3 -->|"CDC / Debezium"| DB2 end subgraph Phase3["Phase 3: Independent"] C1["New Service"] --> DB4["New DB
Source of Truth"] C2["Legacy App
(sunset)"] -.->|"Read-only"| DB4 end Phase1 -->|"Migrate"| Phase2 Phase2 -->|"Cutover"| Phase3 style Phase1 fill:#fff3e0,stroke:#ff9800,color:#2c3e50 style Phase2 fill:#e3f2fd,stroke:#2196F3,color:#2c3e50 style Phase3 fill:#e8f5e9,stroke:#4CAF50,color:#2c3e50

3 phases of data transition: Shared → CDC Sync → Independent

Anti-pattern: Permanent Shared Database

If the new service still reads from and writes to the legacy database directly, you haven't actually modernized — you've just created a distributed monolith. Phase 1 (shared DB) should only be a temporary transitional step, not the final state.

4.1. Change Data Capture (CDC) with Debezium

CDC enables real-time data synchronization from the legacy DB to the new DB without modifying legacy code:

// Debezium connector config for SQL Server
{
  "name": "legacy-order-connector",
  "config": {
    "connector.class": "io.debezium.connector.sqlserver.SqlServerConnector",
    "database.hostname": "legacy-db.internal",
    "database.port": "1433",
    "database.dbname": "LegacyERP",
    "table.include.list": "dbo.tblOrder_Master,dbo.tblOrder_Detail",
    "topic.prefix": "legacy",
    "schema.history.internal.kafka.topic": "schema-changes.legacy"
  }
}

5. Five-Phase Implementation Roadmap

Phase 1 — Set Up Facade (Week 1–2)
Deploy YARP reverse proxy, route 100% of traffic through YARP to the legacy app. No logic changes — just add a proxy layer. Measure baseline metrics (latency, error rate).
Phase 2 — Pilot Service (Week 3–6)
Choose a simple bounded context with few dependencies for the first migration (e.g., Product Catalog). Build the ACL, set up CDC, canary deploy 5% → 25% → 100% traffic.
Phase 3 — Scale Out (Month 2–6)
Migrate subsequent bounded contexts in order of business value priority. Each context repeats the Transform → Coexist → Eliminate cycle.
Phase 4 — Data Independence (Month 6–12)
Move each service to its own database. Eliminate shared database access. Establish event-driven communication between services.
Phase 5 — Sunset Legacy (Month 12–18)
Shut down the legacy app once 100% of traffic is served by the new system. Decommission legacy infrastructure. Archive old code and database.

6. Choosing the Right Module to Migrate First

Migration order directly impacts the speed and risk of the entire project. Evaluate each module against 4 criteria:

CriteriaWeightExplanation
Business Value30%Which module delivers the most business value when improved?
Technical Complexity25%Number of dependencies, stored procedures, shared state
Change Frequency25%Frequently updated modules → migrate early to boost velocity
Legacy Coupling20%Low dependency on other modules → easier to extract

Golden Rule

Start with modules that have high business value + low coupling. This creates a quick win to prove value to stakeholders while minimizing technical risk. Avoid starting with the most complex core module — that's the fastest way to kill a migration project.

7. Monitoring and Observability During Migration

When traffic is split between legacy and new services, you need to monitor both to detect regressions early:

graph TD
    YARP["YARP Facade"] -->|"Metrics + Traces"| OTel["OpenTelemetry
Collector"] New["New Services"] -->|"Metrics + Traces"| OTel Legacy["Legacy App"] -->|"Metrics"| OTel OTel --> Dash["Dashboard
Compare latency, error rate
legacy vs new"] style YARP fill:#e94560,stroke:#fff,color:#fff style New fill:#4CAF50,stroke:#fff,color:#fff style Legacy fill:#ff9800,stroke:#fff,color:#fff style OTel fill:#2c3e50,stroke:#fff,color:#fff style Dash fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50

Observability pipeline comparing performance between legacy and new services

Key metrics to track:

  • Latency P50/P95/P99: The new service must not be slower than legacy
  • Error rate: Compare 5xx error rates between legacy vs new service per endpoint
  • Data consistency: Verify results between legacy and new service using shadow traffic
  • Migration progress: % of traffic shifted to new service (target: steady increase each sprint)

8. Common Mistakes to Avoid

Anti-patternConsequenceSolution
Permanent Shared DatabaseDistributed monolith — worse than the original monolithPlan data migration from day one, use CDC for sync
Migrating by layer instead of featureUI migrated but backend still legacy → no real valueMigrate by vertical slice (UI + API + DB together)
Missing ACLNew domain model contaminated by legacy designAlways have a translation layer between old and new
Migrating too much at onceTeam overload, insufficient testing, complex rollbacksOne bounded context at a time, complete before moving on
No rollback planCritical bugs with no fast path back to legacyYARP route switch — redirect traffic to legacy in seconds

9. When to Use (and Not Use) Strangler Fig

Use when

  • Large monolith with clearly defined bounded contexts
  • Need to continue delivering features during migration
  • Team lacks resources for a full simultaneous rewrite
  • System is running in production with zero-downtime requirement
  • Legacy app has an HTTP interface (web app, API) — easy to place a facade in front

Avoid when

  • Small, simple system — a rewrite is faster and cheaper
  • Legacy app is batch processing / background jobs with no HTTP endpoints
  • Business logic is too coupled — cannot split into bounded contexts
  • No test coverage on legacy — no way to verify behavior parity

10. Conclusion

The Strangler Fig Pattern isn't a silver bullet — but it's the most practical strategy for the majority of enterprise modernization projects. Instead of betting everything on a Big Bang Rewrite, you reduce risk by delivering value incrementally, validating at each step, and always having a rollback path.

With YARP as the facade, feature toggles controlling rollout, ACL protecting the domain model, and CDC synchronizing data — you have a complete toolkit in the .NET ecosystem to execute a safe, controlled migration. Start small, prove value, then scale gradually.

References: