Explain the strangler pattern for migrating to microservices.

6 minadvancedmicroservicesmigrationstrangler-patternarchitecture

Quick Answer

The Strangler Fig pattern incrementally migrates a monolith to microservices by placing a facade/proxy in front of it and gradually routing specific features to new services, retiring the old code piece by piece. This avoids a risky big-bang rewrite, keeps the system running throughout, and lets you validate each slice. Migration completes when the facade no longer routes anything to the original monolith.

Detailed Answer

The Strangler Pattern (named after strangler fig trees that grow around existing trees) is an incremental approach to migrating from a monolithic application to microservices by gradually replacing specific pieces of functionality with new services.

Migration Strategy:

Phase 1: Setup Strangler Facade

// API Gateway/Proxy that routes to monolith or microservices
public class StranglerFacadeMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IConfiguration _configuration;
    
    public async Task InvokeAsync(HttpContext context)
    {
        var path = context.Request.Path.Value;
        
        // Route new functionality to microservices
        if (path.StartsWith("/api/products"))
        {
            await ProxyToMicroservice(context, "ProductService");
        }
        else if (path.StartsWith("/api/orders"))
        {
            await ProxyToMicroservice(context, "OrderService");
        }
        else
        {
            // Route everything else to legacy monolith
            await ProxyToMonolith(context);
        }
    }
    
    private async Task ProxyToMicroservice(HttpContext context, string serviceName)
    {
        var serviceUrl = _configuration[$"Services:{serviceName}:Url"];
        // Forward request to microservice
        await ForwardRequest(context, serviceUrl);
    }
    
    private async Task ProxyToMonolith(HttpContext context)
    {
        var monolithUrl = _configuration["Monolith:Url"];
        await ForwardRequest(context, monolithUrl);
    }
}

Phase 2: Extract First Service

// Original Monolith - Product Module
public class MonolithProductController : ControllerBase
{
    private readonly MonolithDbContext _dbContext;
    
    [HttpGet("api/products/{id}")]
    public async Task GetProduct(int id)
    {
        var product = await _dbContext.Products
            .Include(p => p.Category)
            .Include(p => p.Inventory)
            .FirstOrDefaultAsync(p => p.Id == id);
        
        return Ok(product);
    }
}

// Step 1: Create new Product Microservice
public class ProductMicroserviceController : ControllerBase
{
    private readonly IProductRepository _repository;
    
    [HttpGet("api/products/{id}")]
    public async Task GetProduct(int id)
    {
        var product = await _repository.GetByIdAsync(id);
        return Ok(product);
    }
}

// Step 2: Synchronize data between monolith and microservice during transition
public class ProductDataSyncService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Sync from monolith to microservice
            var products = await _monolithRepository.GetRecentlyUpdatedAsync();
            
            foreach (var product in products)
            {
                await _microserviceRepository.UpsertAsync(product);
            }
            
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}

Phase 3: Implement Anti-Corruption Layer

// Anti-corruption layer to translate between monolith and microservice models
public class ProductAdapter
{
    // Convert monolith model to microservice model
    public ProductDto ToMicroserviceModel(MonolithProduct monolithProduct)
    {
        return new ProductDto
        {
            Id = monolithProduct.ProductId,
            Name = monolithProduct.ProductName,
            Price = monolithProduct.UnitPrice,
            SKU = monolithProduct.StockKeepingUnit,
            Category = new CategoryDto
            {
                Id = monolithProduct.CategoryId,
                Name = monolithProduct.CategoryName
            }
        };
    }
    
    // Convert microservice model back to monolith format if needed
    public MonolithProduct ToMonolithModel(ProductDto microserviceProduct)
    {
        return new MonolithProduct
        {
            ProductId = microserviceProduct.Id,
            ProductName = microserviceProduct.Name,
            UnitPrice = microserviceProduct.Price,
            StockKeepingUnit = microserviceProduct.SKU,
            CategoryId = microserviceProduct.Category.Id
        };
    }
}

// Service that uses both systems during migration
public class HybridProductService
{
    private readonly MonolithProductService _monolithService;
    private readonly ProductMicroserviceClient _microserviceClient;
    private readonly IFeatureManager _featureManager;
    
    public async Task GetProductAsync(int productId)
    {
        // Feature flag to gradually shift traffic
        if (await _featureManager.IsEnabledAsync("UseProductMicroservice"))
        {
            return await _microserviceClient.GetProductAsync(productId);
        }
        else
        {
            var monolithProduct = await _monolithService.GetProductAsync(productId);
            return _adapter.ToMicroserviceModel(monolithProduct);
        }
    }
}

Phase 4: Gradual Migration with Feature Flags

// appsettings.json
{
  "FeatureManagement": {
    "UseProductMicroservice": {
      "EnabledFor": [
        {
          "Name": "Percentage",
          "Parameters": {
            "Value": 10  // Start with 10% of traffic
          }
        }
      ]
    }
  }
}

// Program.cs
builder.Services.AddFeatureManagement();

// Gradually increase percentage: 10% → 25% → 50% → 75% → 100%
public class GradualMigrationService
{
    private readonly IFeatureManager _featureManager;
    
    public async Task ExecuteWithFallback(
        Func<Task> microserviceAction,
        Func<Task> monolithAction)
    {
        if (await _featureManager.IsEnabledAsync("UseProductMicroservice"))
        {
            try
            {
                return await microserviceAction();
            }
            catch (Exception ex)
            {
                // Fallback to monolith if microservice fails
                _logger.LogWarning(ex, "Microservice failed, falling back to monolith");
                return await monolithAction();
            }
        }
        
        return await monolithAction();
    }
}

Phase 5: Database Migration

// Step 1: Read from monolith, write to both
public class DualWriteProductRepository : IProductRepository
{
    private readonly MonolithDbContext _monolithDb;
    private readonly MicroserviceDbContext _microserviceDb;
    
    public async Task AddAsync(Product product)
    {
        // Write to monolith first (existing system)
        await _monolithDb.Products.AddAsync(product);
        await _monolithDb.SaveChangesAsync();
        
        // Also write to microservice database
        try
        {
            await _microserviceDb.Products.AddAsync(product);
            await _microserviceDb.SaveChangesAsync();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to write to microservice DB");
            // Don't fail - monolith is still source of truth
        }
        
        return product;
    }
}

// Step 2: Backfill historical data
public class DataMigrationService
{
    public async Task MigrateProductsAsync()
    {
        var batchSize = 1000;
        var offset = 0;
        
        while (true)
        {
            var products = await _monolithDb.Products
                .Skip(offset)
                .Take(batchSize)
                .ToListAsync();
            
            if (!products.Any())
                break;
            
            await _microserviceDb.Products.AddRangeAsync(products);
            await _microserviceDb.SaveChangesAsync();
            
            offset += batchSize;
            _logger.LogInformation($"Migrated {offset} products");
        }
    }
}

// Step 3: Switch reads to microservice, continue dual writes
// Step 4: Stop writing to monolith, read/write only to microservice
// Step 5: Decommission monolith database access

Phase 6: Complete Migration and Cleanup

// Remove strangler facade routing
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        // Remove: app.UseMiddleware();
        
        // Direct routing to microservices only
        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

// Decommission monolith code
// Remove MonolithProductService, MonolithDbContext, etc.
// Update all references to use microservice clients directly

public class ProductService
{
    private readonly ProductMicroserviceClient _client;
    
    // No more reference to monolith
    public async Task GetProductAsync(int id)
    {
        return await _client.GetProductAsync(id);
    }
}

Migration Checklist:

  1. ✅ Set up API Gateway/Strangler Facade
  2. ✅ Identify bounded context to extract
  3. ✅ Create new microservice
  4. ✅ Implement anti-corruption layer
  5. ✅ Set up dual-write for data
  6. ✅ Migrate historical data
  7. ✅ Use feature flags for gradual rollout
  8. ✅ Monitor both systems
  9. ✅ Switch reads to microservice
  10. ✅ Stop writes to monolith
  11. ✅ Decommission monolith components
  12. ✅ Remove strangler facade

Best Practices:

  • Start with least dependent modules
  • Use feature flags for safe rollback
  • Maintain backward compatibility
  • Monitor performance closely
  • Have rollback plan ready
  • Communicate with stakeholders
  • Document the migration process

Summary

This guide covered essential microservices concepts in .NET Core:

  • Architecture: Understanding microservices vs monolithic approaches
  • Communication: Synchronous (HTTP, gRPC) and asynchronous (message queues)
  • Patterns: API Gateway, Circuit Breaker, Saga, Strangler
  • Infrastructure: Service discovery, containers, orchestration
  • Data: Eventual consistency, distributed transactions

Key Takeaways:

  • Microservices add complexity but provide scalability and flexibility
  • Choose the right patterns for your use case
  • Invest in proper infrastructure (containers, orchestration, monitoring)
  • Migrate incrementally using patterns like Strangler
  • Plan for failure with Circuit Breaker and resilience patterns

Recommended Tools for .NET Core:

  • API Gateway: Ocelot, YARP
  • Service Discovery: Consul, Eureka, Kubernetes
  • Messaging: RabbitMQ, Azure Service Bus, Kafka
  • Resilience: Polly
  • Containers: Docker, Kubernetes
  • Monitoring: Application Insights, Prometheus, Grafana

Related Resources