Strangler Fig Pattern Example: A Worked Case Study with Metrics and Reconciliation Data

Illustrative strangler fig pattern example: a 380K LOC VB6 to .NET walkthrough with a 14-month timeline and a dual-write reconciliation loop.

The Strangler Fig pattern is usually sold as gradual, low-risk modernization. In practice, many attempts stall in the first 90 days and never replace their first monolith component.

This guide walks through a worked example: a 14-month migration of a 380K LOC VB6 pricing engine to .NET 8, including the reconciliation loop designed to catch pricing discrepancies. The figures, including the $4.2M exposure estimate, are illustrative rather than audited results.

Research Methodology

This guide combines one detailed worked example (an insurance SaaS pricing engine migration) with general patterns for where strangler projects stall. The worked example is a single illustrative walkthrough, not a measured cohort of projects.


Why Strangler Attempts Fail in the First 90 Days

Failure Analysis

Failure ModePrimary Symptom
Started at UI layerNew UI tightly coupled to legacy backend
No stable semantic boundaryEndless scope creep ("just one more field")
Treated legacy DB as immutableDual-write problems, data sync failures
Underestimated observabilityCan't debug distributed system
Cultural resistance (no DevOps)Teams refuse to own new services

Key principle: Early velocity is a warning signal. A team that has extracted almost none of the monolith's functionality in the first 90 days is stalling, and the plan needs correcting.

Anti-Pattern #1: Trying to Strangle at UI Layer First

Illustrative scenario: E-commerce platform (failed)

Approach: Built new React admin panel to replace legacy JSP UI
Problem: New UI made dozens of API calls to the legacy monolith for a single page load
Result: "Modern" UI inherited all legacy performance/reliability issues

Lesson: UI is the tip of the iceberg. Strangling a UI without owning backend logic creates a distributed frontend—worst of both worlds.

Anti-Pattern #2: No Stable Semantic Boundary → Scope Creep

Illustrative example: "Customer Service" extraction (failed)

WeekRequested ChangeImpact on Boundary
Week 1"Add last order date to customer API"Pulls in Order domain
Week 3"Include shipping status"Pulls in Fulfillment domain
Week 5"Show return history"Pulls in Returns domain
Week 8CTO: "This service now depends on 6 domains. Start over."Project reset

Outcome: 8 weeks wasted. Service never deployed.

Anti-Pattern #3: Treating the Legacy Database as Immutable (Dual-Write Problems)

Dual-write approaches compared

Dual-Write StrategyNotes
No reconciliationData drift goes undetected
Manual reconciliationHuman error, slow
Automated reconciliationRequires upfront investment; catches drift early

The Only Three Places to Cut a New Root

There are three viable starting points, in ascending order of risk:

Root Type A: New Business Capability (Greenfield)

Risk: Lowest

Worked example: Proactive Delivery Notifications

MetricValue
Legacy systemMonolithic e-commerce (2.1M LOC)
New serviceReal-time delivery tracker
IntegrationEvent listener only (no legacy coupling)
Timeline6 weeks design → code → production

Why It Worked: Zero legacy coupling. Service consumed events but owned no legacy data.

Root Type B: Read-Only Facade (Anti-Corruption Layer)

Risk: Moderate

Requires: CDC (Change Data Capture) for event interception, denormalized read model

Worked example: Product Catalog Facade

  • Before: Direct DB queries to a 47-table normalized schema
  • After: Event-sourced read model (Elasticsearch) that serves reads without touching the legacy schema
  • CDC Tool: Debezium → Kafka → custom projector
  • Example timeline: 14 weeks (4 weeks CDC setup, 10 weeks service development)

Root Type C: Write-Path Interception (Highest Risk/Reward)

Risk: Highest

Only attempt if: Team has distributed systems expertise, business tolerates 12+ month dual-write

This is the approach used in the pricing engine example (detailed below).


Concrete Example: Strangling the Pricing Engine

Illustrative walkthrough. The figures below show what a project like this could look like; they are not audited results from a named engagement.

Project: Insurance SaaS pricing engine migration
Codebase: 380,418 LOC VB6 + 127 SQL Server stored procedures
Timeline: 14 months
Team: 5 engineers (3 backend, 1 DevOps, 1 QA)
Total Cost: $1.24M (salary + infra)
Illustrative business value: $4.2M estimated discrepancy exposure the reconciliation loop was designed to catch, plus $680K annual infrastructure savings

The Initial State: Day 0 Metrics

MetricValueImpact
Median pricing latency1,247msConversion drop-off during quote flow
P95 latency3,180ms8% of users abandoned quotes
Deployment frequency1x per quarterBusiness can't iterate on pricing models
Pricing bug MTTR4.2 daysManual investigation in VB6 code
Annual infrastructure cost$840KOver-provisioned on-prem servers

Phase 1: Command Interceptor + Dual-Write (Day 0-120)

Week 1-2: Built reverse proxy in Go, deployed in shadow mode (log-only, no routing)
Week 3-8: Developed .NET 8 pricing service (single pricing rule: base premium calculation)
Week 9-17: Enabled dual-write mode

Dual-Write Architecture:

public async Task<PricingResult> CalculatePriceAsync(PricingRequest request)
{
    var trace = Activity.Current; // OpenTelemetry trace
    
    // Fire-and-forget to new service for validation
    _ = Task.Run(async () => {
        var newResult = await newPricingServiceClient.CalculateAsync(request);
        await reconciler.LogResult(request.Id, "dotnet", newResult);
    });
    
    // Primary blocking call to legacy VB6
    var legacyResult = await legacyVb6Client.CalculateAsync(request);
    await reconciler.LogResult(request.Id, "vb6", legacyResult);
    
    trace.SetTag("routing", "legacy");
    return legacyResult; // Only legacy result returned to user
}

Day 120 Metrics:

MetricLegacy (VB6)New (.NET 8)Delta
Median latency1,247ms38ms-97%
P95 latency3,180ms94ms-97%
Throughput180 req/sec2,400 req/sec+1,233%
Memory per request18MB2.1MB-88%

Phase 2: Reconciliation & Validation (Day 121-300)

Automated Reconciliation Loop:

  • Frequency: Every 5 minutes
  • Events processed: 12.4M pricing calculations
  • Mismatches detected: 847 (0.007% of events)

Mismatch Breakdown:

Mismatch TypeCount% of TotalRoot Cause
Rounding differences41249%VB6 used Decimal, .NET used double (fixed Week 14)
Tax calculation edge case21826%Leap year bug in VB6 (documented, accepted)
Multi-state discount logic12715%Undocumented VB6 business rule (added to .NET Week 22)
Timezone handling648%VB6 assumed EST, .NET used UTC (fixed Week 18)
Floating point precision263%< $0.01 difference (accepted as noise)

Reconciliation Code (Simplified):

# Reconciler job (every 5min)
def reconcile_batch():
    unreconciled = event_store.fetch_pairs(
        last_15_min=True,
        status='unreconciled'
    )
    
    for (legacy_event, modern_event) in unreconciled:
        diff = deep_compare(legacy_event.result, modern_event.result)
        
        if diff.is_match():
            event_store.mark_reconciled(legacy_event, modern_event)
        else:
            alert_slack(
                channel='#pricing-migration',
                diff=diff,
                events=[legacy_event, modern_event]
            )
            event_store.mark_mismatched(legacy_event, modern_event, diff)

# Runs continuously for the duration of the dual-write period

Kill Criteria: 4 consecutive weeks with 0 mismatches (achieved Week 52)

Phase 3: Traffic Cut-Over (Day 301-360)

Week 43: Enabled 5% traffic to new service (canary deployment)
Week 44-48: Gradual ramp: 5% → 15% → 35% → 60% → 85%
Week 49: 100% traffic to new service, legacy VB6 in passive monitoring
Week 52: 4 weeks at 0 mismatches → decommissioned legacy system

Final Production Metrics:

MetricBefore (VB6)After (.NET 8)Improvement
Median latency1,247ms32ms-97.4%
P99 latency4,820ms78ms-98.4%
Error rate0.18%0.004%-97.8%
Infrastructure cost$840K/year$160K/year-$680K
Deployment frequency4x/year52x/year+1,200%
Pricing bug MTTR4.2 days1.8 hours-96%

The Reconciliation Loop Built to Catch $4.2M in Exposure

Without automated reconciliation, data discrepancies are likely. In this illustrative scenario, transaction volume of 840K pricing calculations per month puts roughly $4.2M in incorrect quotes and invoices at risk over 14 months if drift goes undetected. That is a modeled exposure, not a measured loss avoided.

Idempotent Event Store Design

Every pricing calculation fired a PricingCalculationRecorded event with:

  • Deterministic ID: SHA-256 hash of request payload
  • Input: Full request (product, customer, coverage options)
  • Output: Calculated premium + breakdown
  • Source: legacy-vb6 or modern-dotnet
  • Timestamp: ISO 8601 with nanosecond precision

Event Schema (v2):

{
  "eventId": "sha256(request_payload)",
  "eventType": "PricingCalculationRecorded",
  "version": "v2",
  "source": "modern-dotnet",
  "timestamp": "2024-08-15T14:23:18.472Z",
  "request": {
    "customerId": "C-94721",
    "productId": "AUTO-PREMIUM",
    "coverageOptions": ["collision", "comprehensive"],
    "vehicleYear": 2022,
    "zipCode": "10001"
  },
  "result": {
    "premium": 1847.23,
    "breakdown": {
      "base": 1200.00,
      "collisionSurcharge": 420.00,
      "multiCarDiscount": -180.00,
      "stateTax": 407.23
    }
  }
}

CRDT-Based Reconciler Performance

14-Month Operational Data:

MetricValue
Events processed12.4M
Avg processing time1.8 seconds
Mismatches detected847 (0.007%)
False positives3 (0.0004%)
Downtime incidents0
Storage cost$2,400 (S3 event log)

Mismatch Resolution Timeline:

Issue TypeMedian Time to FixMax Time to Fix
Rounding/precision2.4 days8 days
Undocumented business rule5.8 days14 days
Timezone/date handling3.2 days11 days

Kill Criteria Achievement:

  • Week 48: First week with 0 mismatches
  • Week 49-52: 4 consecutive weeks at 0 mismatches
  • Week 53: Reconciler decommissioned

Success Pattern: 9 Prerequisites

PrerequisiteWhy It Matters
Comprehensive test coverage for legacyBaseline confidence
Stable semantic boundary (DDD)Prevents scope creep
Stakeholder buy-in for dual operationsBudget stability
Single accountable ownerDecision velocity
Robust monitoring already in placeDebugging capability
Data ownership plan documentedAvoids data hell
Interception point identifiedImplementation clarity
Reconciliation strategy designedData integrity
Kill-switch (instant rollback)Risk mitigation

Missing several of these prerequisites is a strong signal to close the gaps before you start.


When to Abandon Strangler for Big Bang

Decision Matrix

FactorBetter Fit
Modular business logicStrangler
Big ball of mud (no boundaries)Big Bang
Deep team knowledge of legacyStrangler
Black box (no docs, devs gone)Big Bang
Business-critical (no downtime)Strangler
Can tolerate maintenance freezeBig Bang

Illustrative scenario: When Big Bang can win

Company: Healthcare analytics (4.2M LOC legacy Perl)
Problem: No module boundaries, 40% code coverage unknown authors
Decision: 9-month rewrite in TypeScript (greenfield)
Outcome: Successful rewrite with a short cutover
Why: Strangler would require understanding Perl codebase (impossible), Big Bang allowed clean-slate architecture


Strangler vs. Big Bang: Comparing the Approaches

How the Approaches Compare

ApproachDelivery RiskWhere It Fits
Strangler FigLower, incrementalModular logic, business-critical systems, teams that know the legacy code
Big Bang RewriteHigherUnmaintainable legacy (no docs, no experts), or a tolerable maintenance freeze

Nuance: Big Bang becomes the more realistic option when the legacy system is truly unmaintainable (no docs, no experts) and no strangler boundary can be drawn.


Testing Strategy: Why Contract Testing Matters

Test Distribution Shift

The percentages below are an illustrative rule of thumb for how emphasis shifts, not measured data.

Test TypeMonolith %During Strangler %Post-Migration %
Unit (within service)40%55%70%
Contract (API agreements)5%35%25%
Integration (DB/external)25%8%4%
E2E (full system)30%2%1%

Contract Testing Example (Pact):

// Consumer contract: OrderService expects this from PricingService
describe("Pricing API Contract", () => {
  it("returns premium for valid request", async () => {
    await provider
      .given("customer C-94721 exists")
      .uponReceiving("pricing request for AUTO-PREMIUM")
      .withRequest({
        method: "POST",
        path: "/pricing/calculate",
        body: { customerId: "C-94721", productId: "AUTO-PREMIUM" }
      })
      .willRespondWith({
        status: 200,
        body: { premium: like(1847.23), breakdown: like({}) }
      });
    
    const result = await pricingClient.calculate(request);
    expect(result.premium).toBeGreaterThan(0);
  });
});

Contract testing is what lets teams cut over one service at a time without a full end-to-end environment.


Measuring Success: Key Metrics

From the pricing engine example:

MetricTargetWorked Example (Pricing Engine)
Traffic shift to new service100%100% (Week 49)
Latency improvement>50%97.4%
Error rate drop>70%97.8%
Reconciliation mismatches<0.01% final 30 days0% (Weeks 49-52)
Infrastructure cost reductionVaries81% ($680K)
Deployment frequency increase>500%1,200%

Checklist: Your Readiness Score

Rate each item 0-10:

CategoryQuestionYour Score
BoundariesWe have identified clear, stable bounded contexts (DDD)__/10
TestingLegacy component has >70% automated test coverage__/10
MonitoringWe have distributed tracing + centralized logging ready__/10
DataWe have a documented data ownership + sync strategy__/10
TeamTeam has distributed systems expertise (Kafka, CDC, sagas)__/10
BudgetStakeholders approved 12-18 month dual-operations cost__/10
CultureTeams willing to own full lifecycle (DevOps)__/10

Total: __/70

  • 60-70: Greenlight—proceed with confidence
  • 50-59: Viable—address gaps in parallel
  • 40-49: Risky—invest in prerequisites first
  • <40: Not ready—stay monolithic or modular monolith

Further Reading


Evidence note: Earlier versions of this guide cited project counts, success rates, and median costs and timelines from a set of strangler projects that could not be reproduced from available records; they have been withdrawn. The pricing engine walkthrough is a single worked example whose project records were not available for review, so read its figures as illustrative rather than audited. See our methodology.

Need unbiased vendor guidance for your strangler fig migration? Explore our Application Modernization hub or read our methodology for how we research migration projects.