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.
On this page
- Overview
- Research Methodology
- Why Strangler Attempts Fail in the First 90 Days
- Failure Analysis
- Anti-Pattern #1: Trying to Strangle at UI Layer First
- Anti-Pattern #2: No Stable Semantic Boundary → Scope Creep
- Anti-Pattern #3: Treating the Legacy Database as Immutable (Dual-Write Problems)
- The Only Three Places to Cut a New Root
- Root Type A: New Business Capability (Greenfield)
- Root Type B: Read-Only Facade (Anti-Corruption Layer)
- Root Type C: Write-Path Interception (Highest Risk/Reward)
- Concrete Example: Strangling the Pricing Engine
- The Initial State: Day 0 Metrics
- Phase 1: Command Interceptor + Dual-Write (Day 0-120)
- Phase 2: Reconciliation & Validation (Day 121-300)
- Phase 3: Traffic Cut-Over (Day 301-360)
- The Reconciliation Loop Built to Catch $4.2M in Exposure
- Idempotent Event Store Design
- CRDT-Based Reconciler Performance
- Success Pattern: 9 Prerequisites
- When to Abandon Strangler for Big Bang
- Decision Matrix
- Strangler vs. Big Bang: Comparing the Approaches
- How the Approaches Compare
- Testing Strategy: Why Contract Testing Matters
- Test Distribution Shift
- Measuring Success: Key Metrics
- Checklist: Your Readiness Score
- Further Reading
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 Mode | Primary Symptom |
|---|---|
| Started at UI layer | New UI tightly coupled to legacy backend |
| No stable semantic boundary | Endless scope creep ("just one more field") |
| Treated legacy DB as immutable | Dual-write problems, data sync failures |
| Underestimated observability | Can'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)
| Week | Requested Change | Impact 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 8 | CTO: "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 Strategy | Notes |
|---|---|
| No reconciliation | Data drift goes undetected |
| Manual reconciliation | Human error, slow |
| Automated reconciliation | Requires 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
| Metric | Value |
|---|---|
| Legacy system | Monolithic e-commerce (2.1M LOC) |
| New service | Real-time delivery tracker |
| Integration | Event listener only (no legacy coupling) |
| Timeline | 6 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
| Metric | Value | Impact |
|---|---|---|
| Median pricing latency | 1,247ms | Conversion drop-off during quote flow |
| P95 latency | 3,180ms | 8% of users abandoned quotes |
| Deployment frequency | 1x per quarter | Business can't iterate on pricing models |
| Pricing bug MTTR | 4.2 days | Manual investigation in VB6 code |
| Annual infrastructure cost | $840K | Over-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:
| Metric | Legacy (VB6) | New (.NET 8) | Delta |
|---|---|---|---|
| Median latency | 1,247ms | 38ms | -97% |
| P95 latency | 3,180ms | 94ms | -97% |
| Throughput | 180 req/sec | 2,400 req/sec | +1,233% |
| Memory per request | 18MB | 2.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 Type | Count | % of Total | Root Cause |
|---|---|---|---|
| Rounding differences | 412 | 49% | VB6 used Decimal, .NET used double (fixed Week 14) |
| Tax calculation edge case | 218 | 26% | Leap year bug in VB6 (documented, accepted) |
| Multi-state discount logic | 127 | 15% | Undocumented VB6 business rule (added to .NET Week 22) |
| Timezone handling | 64 | 8% | VB6 assumed EST, .NET used UTC (fixed Week 18) |
| Floating point precision | 26 | 3% | < $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:
| Metric | Before (VB6) | After (.NET 8) | Improvement |
|---|---|---|---|
| Median latency | 1,247ms | 32ms | -97.4% |
| P99 latency | 4,820ms | 78ms | -98.4% |
| Error rate | 0.18% | 0.004% | -97.8% |
| Infrastructure cost | $840K/year | $160K/year | -$680K |
| Deployment frequency | 4x/year | 52x/year | +1,200% |
| Pricing bug MTTR | 4.2 days | 1.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-vb6ormodern-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:
| Metric | Value |
|---|---|
| Events processed | 12.4M |
| Avg processing time | 1.8 seconds |
| Mismatches detected | 847 (0.007%) |
| False positives | 3 (0.0004%) |
| Downtime incidents | 0 |
| Storage cost | $2,400 (S3 event log) |
Mismatch Resolution Timeline:
| Issue Type | Median Time to Fix | Max Time to Fix |
|---|---|---|
| Rounding/precision | 2.4 days | 8 days |
| Undocumented business rule | 5.8 days | 14 days |
| Timezone/date handling | 3.2 days | 11 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
| Prerequisite | Why It Matters |
|---|---|
| Comprehensive test coverage for legacy | Baseline confidence |
| Stable semantic boundary (DDD) | Prevents scope creep |
| Stakeholder buy-in for dual operations | Budget stability |
| Single accountable owner | Decision velocity |
| Robust monitoring already in place | Debugging capability |
| Data ownership plan documented | Avoids data hell |
| Interception point identified | Implementation clarity |
| Reconciliation strategy designed | Data 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
| Factor | Better Fit |
|---|---|
| Modular business logic | Strangler |
| Big ball of mud (no boundaries) | Big Bang |
| Deep team knowledge of legacy | Strangler |
| Black box (no docs, devs gone) | Big Bang |
| Business-critical (no downtime) | Strangler |
| Can tolerate maintenance freeze | Big 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
| Approach | Delivery Risk | Where It Fits |
|---|---|---|
| Strangler Fig | Lower, incremental | Modular logic, business-critical systems, teams that know the legacy code |
| Big Bang Rewrite | Higher | Unmaintainable 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 Type | Monolith % | 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:
| Metric | Target | Worked Example (Pricing Engine) |
|---|---|---|
| Traffic shift to new service | 100% | 100% (Week 49) |
| Latency improvement | >50% | 97.4% |
| Error rate drop | >70% | 97.8% |
| Reconciliation mismatches | <0.01% final 30 days | 0% (Weeks 49-52) |
| Infrastructure cost reduction | Varies | 81% ($680K) |
| Deployment frequency increase | >500% | 1,200% |
Checklist: Your Readiness Score
Rate each item 0-10:
| Category | Question | Your Score |
|---|---|---|
| Boundaries | We have identified clear, stable bounded contexts (DDD) | __/10 |
| Testing | Legacy component has >70% automated test coverage | __/10 |
| Monitoring | We have distributed tracing + centralized logging ready | __/10 |
| Data | We have a documented data ownership + sync strategy | __/10 |
| Team | Team has distributed systems expertise (Kafka, CDC, sagas) | __/10 |
| Budget | Stakeholders approved 12-18 month dual-operations cost | __/10 |
| Culture | Teams 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
- Monolith to Microservices: Complete Migration Guide
- Domain-Driven Design for Legacy Systems
- Change Data Capture: Implementation Patterns
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.
Chief Analyst, Software Modernization Intelligence · 10+ years B2B market research
Published: ·Updated: