Strangler Fig — Legacy Migration Strategy
Incrementally Modernizing Integration Systems
Introduction
The Strangler Fig pattern is a migration strategy that allows you to gradually replace a legacy system with a modern one without requiring a "big bang" cutover. Named after the strangler fig tree that grows around and eventually replaces its host tree, this pattern enables continuous migration, reducing risk and allowing teams to validate each step. For Azure integration workloads, this is particularly valuable when modernizing ASMX web services, BizTalk orchestrations, or legacy message brokers.
This comprehensive guide covers:
- Pattern fundamentals — Understanding strangler fig
- Migration strategies — Incrementally replacing components
- Azure implementation — Using API Management, Functions
- Data migration — Handling state and migrations
- Testing and validation — Ensuring quality during migration
Understanding the Pattern
How Strangler Fig Works
┌─────────────────────────────────────────────────────────────────────┐
│ STRANGLER FIG PATTERN │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ BEFORE: Monolithic Legacy │
│ ─────────────────────────────────── │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ LEGACY SYSTEM │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Order │ │Customer │ │Inventory │ │ Shipping │ │ │
│ │ │ Service │ │ Service │ │ Service │ │ Service │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ DURING: Strangler in Action │
│ ───────────────────────────── │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ LEGACY │ │ MODERN │ │
│ │ SYSTEM │ │ SYSTEM │ │
│ │ │ │ │ │
│ │ ┌─────────┐ │ │ ┌─────────┐ │ │
│ │ │Customer │ │ │ │Inventory│ │ │
│ │ │ Service │ │ │ │ Service │ │ │
│ │ └─────────┘ │ │ └─────────┘ │ │
│ │ │ │ │ │
│ │ ┌─────────┐ │ │ ┌─────────┐ │ │
│ │ │Shipping │ │ │ │ Order │ │ │
│ │ │ Service │ │ │ │ Service │ │ │
│ │ └─────────┘ │ │ └─────────┘ │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ └──────────┬─────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ API MANAGEMENT │ ← Facade Layer │
│ │ (Strangler) │ │
│ └─────────────────────┘ │
│ │
│ AFTER: Modern System │
│ ───────────────────── │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ MODERN SYSTEM │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Order │ │Customer │ │Inventory │ │ Shipping │ │ │
│ │ │ Service │ │ Service │ │ Service │ │ Service │ │ │
│ │ │ (Azure │ │ (Azure │ │ (Azure │ │ (Azure │ │ │
│ │ │Functions)│ │Functions)│ │Functions)│ │Functions)│ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Migration Phases
┌─────────────────────────────────────────────────────────────────────┐
│ MIGRATION PHASES │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ PHASE 1: FACADE LAYER │
│ ──────────────────── │
│ Deploy API Management as the entry point │
│ Route traffic to legacy (100%) and modern (0%) │
│ Establish monitoring baseline │
│ │
│ PHASE 2: EXTRACT FIRST SERVICE │
│ ───────────────────────────── │
│ Build modern service alongside legacy │
│ Route 5% traffic to modern, 95% to legacy │
│ Validate behavior and performance │
│ │
│ PHASE 3: INCREASE TRAFFIC │
│ ──────────────────────── │
│ Gradually shift to 50%, then 80%, then 95% │
│ Monitor error rates, latency, and user feedback │
│ Stop legacy service when confidence is high │
│ │
│ PHASE 4: REPEAT │
│ ───────────────── │
│ Move to next service │
│ Continue until entire system is modern │
│ │
│ PHASE 5: DECOMMISSION │
│ ────────────────────── │
│ Remove legacy infrastructure │
│ Clean up routing rules │
│ Archive documentation │
│ │
└─────────────────────────────────────────────────────────────────────┘
Azure Implementation
API Management Configuration
{
"apiManagement": {
"name": "integration-strangler",
"resourceGroup": "rg-migration",
"publisherEmail": "platform@company.com",
"sku": "Developer",
"apis": [
{
"name": "orders-api",
"displayName": "Orders API",
"backendService": {
"url": "{{legacy-orders-url}}"
},
"policies": [
{
"name": "route-to-modern",
"conditions": [
{"expression": "{{enable-modern-orders}} == true"}
],
"actions": [
{"set-backend-service": {"url": "{{modern-orders-url}}"}}
]
}
]
}
],
"versioning": {
"scheme": "Header",
"headerName": "x-api-version"
}
}
}
Traffic Routing Policy
<!-- API Management Routing Policy -->
<policies>
<inbound>
<!-- Feature flag based routing -->
<choose>
<when condition="@(context.Variables.GetValueOrDefault<bool>("useModernService"))">
<set-backend-service base-url="https://modern-functions.azurewebsites.net" />
<set-header name="X- routing-Reason" exists-action="override" value="modern-service" />
</when>
<otherwise>
<set-backend-service base-url="https://legacy-api.azurewebsites.net" />
<set-header name="X- routing-Reason" exists-action="override" value="legacy-service" />
</otherwise>
</choose>
<!-- Correlation for tracing -->
<set-header name="X-Correlation-ID" exists-action="override" value="@(Guid.NewGuid().ToString())" />
</inbound>
<backend>
<!-- Circuit breaker for modern service -->
<forward-request timeout="30" />
</backend>
<outbound>
<!-- Ensure consistent response format -->
<choose>
<when condition="@(context.Variables.GetValueOrDefault<string>("routingReason") == "legacy-service")">
<!-- Transform legacy response to modern format -->
<json-transformer />
</when>
</choose>
</outbound>
</policies>
Feature Flag Integration
public class FeatureFlagService
{
private readonly IConfiguration _configuration;
public bool IsModernServiceEnabled(string serviceName)
{
var featureKey = $"features:modern:{serviceName}";
return _configuration.GetValue<bool>(featureKey);
}
public async Task<(string BackendUrl, bool UseModern)> GetRouteTargetAsync(
string serviceName)
{
var useModern = IsModernServiceEnabled(serviceName);
if (useModern)
{
var modernUrl = await GetModernServiceUrlAsync(serviceName);
return (modernUrl, true);
}
var legacyUrl = await GetLegacyServiceUrlAsync(serviceName);
return (legacyUrl, false);
}
}
// Usage in Azure Function
public class OrdersFunction
{
private readonly FeatureFlagService _featureFlags;
public async Task<IActionResult> GetOrders(HttpRequest req)
{
var (url, isModern) = await _featureFlags.GetRouteTargetAsync("orders");
// Call appropriate backend
var response = await _httpClient.GetAsync(url);
return new OkObjectResult(await response.Content.ReadAsStringAsync());
}
}
Data Migration Strategy
Parallel Data Stores
public class ParallelDataMigration
{
private readonly TableClient _legacyTable;
private readonly CosmosClient _modernCosmos;
public async Task MigrateDataIncrementalAsync(
DateTime lastMigrateTime,
CancellationToken ct)
{
// Read from legacy
var legacyRecords = await _legacyTable.QueryAsync<LegacyOrder>(
filter: $"Timestamp gt '{lastMigrateTime}'")
.ToListAsync(ct);
// Transform to modern format
var modernOrders = legacyRecords.Select(TransformToModern).ToList();
// Write to modern
foreach (var order in modernOrders)
{
await _modernCosmos.CreateItemAsync("orders", order);
}
// Update checkpoint
await UpdateMigrationCheckpointAsync(DateTime.UtcNow);
}
private Order TransformToModern(LegacyOrder legacy)
{
return new Order
{
Id = legacy.RowKey,
CustomerId = legacy.PartitionKey,
Items = JsonSerializer.Deserialize<List<OrderItem>>(legacy.ItemsJson),
Total = legacy.TotalAmount,
Status = MapStatus(legacy.Status),
CreatedAt = legacy.Timestamp,
ModifiedAt = legacy.LastModified
};
}
}
Dual-Write Pattern
public class DualWriteService
{
private readonly ILegacyRepository _legacyRepo;
private readonly IModernRepository _modernRepo;
public async Task CreateOrderAsync(Order order)
{
// Write to both systems during migration
var tasks = new List<Task>
{
_legacyRepo.CreateAsync(order),
_modernRepo.CreateAsync(order)
};
try
{
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
// Handle partial failure
await HandleDualWriteFailureAsync(ex, order);
throw;
}
}
private async Task HandleDualWriteFailureAsync(Exception ex, Order order)
{
// If one succeeded, schedule reconciliation
if (_legacyRepo.Exists(order.Id) != _modernRepo.Exists(order.Id))
{
await _reconciliationQueue.SendAsync(new ReconciliationMessage
{
EntityType = "Order",
EntityId = order.Id,
FailureTime = DateTime.UtcNow
});
}
}
}
Testing Strategy
Validation Approach
public class MigrationValidator
{
public async Task ValidateMigrationAsync(string entityType, string entityId)
{
var legacyEntity = await GetLegacyEntityAsync(entityType, entityId);
var modernEntity = await GetModernEntityAsync(entityType, entityId);
// Compare key fields
Assert.Equal(legacyEntity.Id, modernEntity.Id);
Assert.Equal(legacyEntity.CustomerId, modernEntity.CustomerId);
Assert.Equal(legacyEntity.Total, modernEntity.Total);
// Allow for transformation differences
// (e.g., date formats, field naming)
}
public async Task<bool> ValidateTrafficShiftAsync(
string serviceName,
decimal expectedModernPercentage)
{
var requestCount = await GetRequestCountsAsync(serviceName);
var actualPercentage = requestCount.ModernRequests /
(requestCount.ModernRequests + requestCount.LegacyRequests);
// Allow 5% variance
return Math.Abs(actualPercentage - expectedModernPercentage) < 0.05m;
}
public async Task<List<string>> IdentifyDataGapsAsync(string entityType)
{
var legacyIds = await GetAllLegacyIdsAsync(entityType);
var modernIds = await GetAllModernIdsAsync(entityType);
return legacyIds.Except(modernIds).ToList();
}
}
Shadow Mode Testing
{
"shadowTesting": {
"enabled": true,
"targetService": "orders-api",
"mirrorTraffic": true,
"comparison": {
"responseTime": {
"tolerance": "20%"
},
"responseBody": {
"strictEquality": false,
"fieldComparison": ["id", "customerId", "total", "items"]
},
"statusCodes": {
"mustMatch": true
}
},
"alerting": {
"divergenceThreshold": "5%",
"alertChannel": "slack-migration-alerts"
}
}
}
Monitoring During Migration
Key Metrics
┌─────────────────────────────────────────────────────────────────────┐
│ MIGRATION METRICS DASHBOARD │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ TRAFFIC SPLIT: │
│ • Legacy: 80% (target: decreasing) │
│ • Modern: 20% (target: increasing) │
│ │
│ HEALTH METRICS: │
│ • Legacy Error Rate: 0.1% │
│ • Modern Error Rate: 0.05% │
│ • Legacy Latency (p95): 150ms │
│ • Modern Latency (p95): 45ms │
│ │
│ DATA MIGRATION: │
│ • Orders Migrated: 45,000 / 50,000 (90%) │
│ • Last Migration Time: 2024-01-15 14:30 UTC │
│ • Data Gap Count: 3 │
│ │
│ ALERTS: │
│ ⚠ Modern service latency spike detected (resolved) │
│ ✓ Data reconciliation completed │
│ │
└─────────────────────────────────────────────────────────────────────┘
Best Practices
Implementation Checklist
| Practice | Description |
|---|---|
| Start with stable services | Don't migrate complex/brittle services first |
| Feature flags everywhere | Enable quick rollbacks |
| Monitor both paths | Full visibility during migration |
| Data validation early | Verify data integrity before traffic shift |
| Parallel run period | Run both systems simultaneously |
| Document transforms | Capture all mapping logic |
Common Pitfalls
┌─────────────────────────────────────────────────────────────────────┐
│ MIGRATION PITFALLS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ✗ Migrating too much at once │
│ ✗ Not testing data transformation thoroughly │
│ ✗ Ignoring performance differences │
│ ✗ Failing to involve legacy system owners │
│ ✗ Skipping the parallel run phase │
│ ✗ Not having rollback plan │
│ │
└─────────────────────────────────────────────────────────────────────┘
Related Topics
- Anti-Corruption Layer — Protecting modern systems
- API Management Caching — Performance optimization
- Active-Active Multi-Region — Global distribution
Azure Integration Hub - Architect Level Enterprise Integration Patterns