Anti-Corruption Layer Pattern
Protecting Modern Systems from Legacy Complexity
Introduction
When integrating modern Azure integration services with legacy systems, the last thing you want is for legacy quirks, outdated protocols, and inconsistent contracts to pollute your clean architecture. The Anti-Corruption Layer (ACL) is a pattern that acts as a protective buffer between your modern system and external or legacy systems, translating and isolating their complexities while preserving your system's integrity.
This comprehensive guide covers:
- Pattern fundamentals — Understanding ACL
- Implementation approaches — Where and how to apply
- Azure implementation — Using Functions, APIM, Logic Apps
- Translation strategies — Handling different data formats
- Testing ACL behavior — Ensuring reliability
Understanding the Pattern
Role of Anti-Corruption Layer
┌─────────────────────────────────────────────────────────────────────┐
│ ANTI-CORRUPTION LAYER ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ YOUR MODERN SYSTEM │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Order │ │Customer │ │Inventory │ │ Shipping │ │ │
│ │ │ Domain │ │ Domain │ │ Domain │ │ Domain │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ │ Clean Domain Models │ │
│ └────────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────┼────────────────────────────────┐ │
│ │ ANTI-CORRUPTION LAYER │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Translator │ │ Adapter │ │ Facade │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ • Map DTOs │ │ • Protocol │ │ • Simplify │ │ │
│ │ │ • Validate │ │ • Convert │ │ • Combine │ │ │
│ │ │ • Enrich │ │ • Handle │ │ • Cache │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └────────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────┼────────────────────────────────┐ │
│ │ EXTERNAL/LEGACY SYSTEMS │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ SAP ERP │ │ Mainframe │ │ Legacy │ │ │
│ │ │ │ │ AS/400 │ │ ASMX │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
When to Use ACL
┌─────────────────────────────────────────────────────────────────────┐
│ ACL USE CASES │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ✓ Integrating with multiple external APIs with different formats │
│ ✓ Consuming legacy SOAP services with complex schemas │
│ ✓ Bridging between REST and other protocols (GraphQL, gRPC) │
│ ✓ Normalizing inconsistent third-party data │
│ ✓ Adding caching to expensive external calls │
│ ✓ Handling pagination differences │
│ ✓ Managing rate limiting and quotas │
│ ✓ Buffering from upstream changes │
│ │
│ WHEN NOT TO USE: │
│ ✗ Simple 1:1 integrations │
│ ✗ When you control both systems (use direct integration) │
│ ✗ High-performance, low-latency requirements │
│ │
└─────────────────────────────────────────────────────────────────────┘
Azure Implementation
ACL with Azure Functions
public class OrderAntiCorruptionLayer
{
private readonly ILogger<OrderAntiCorruptionLayer> _logger;
private readonly IExternalOrderMapper _mapper;
private readonly ILegacyOrderClient _legacyClient;
// Public interface: Clean domain model
public async Task<Order> GetOrderAsync(string orderId)
{
// Translate internal model to external format
var request = _mapper.ToLegacyRequest(orderId);
try
{
// Call legacy system
var legacyResponse = await _legacyClient.GetOrderAsync(request);
// Translate response to internal model
return _mapper.ToInternalModel(legacyResponse);
}
catch (LegacyServiceException ex)
{
// Handle legacy-specific errors
throw new OrderNotFoundException(orderId, ex);
}
}
public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
{
// Validate incoming request
ValidateRequest(request);
// Transform to legacy format
var legacyRequest = _mapper.ToLegacyCreateRequest(request);
// Call legacy system
var legacyResponse = await _legacyClient.CreateOrderAsync(legacyRequest);
// Transform response
return _mapper.ToInternalModel(legacyResponse);
}
private void ValidateRequest(CreateOrderRequest request)
{
// Business validation - keep external validation separate
if (request.Items == null || !request.Items.Any())
throw new ValidationException("Order must have items");
}
}
// Mapper separates concerns
public class ExternalOrderMapper
{
public LegacyOrderRequest ToLegacyRequest(string orderId)
{
return new LegacyOrderRequest
{
OrderKey = orderId,
Format = "JSON"
};
}
public LegacyCreateOrderRequest ToLegacyCreateRequest(CreateOrderRequest request)
{
return new LegacyCreateOrderRequest
{
CustomerRef = request.CustomerId,
LineItems = request.Items.Select(i => new LegacyLineItem
{
ProductCode = i.ProductCode,
Quantity = i.Quantity,
Price = i.UnitPrice
}).ToList(),
RequestedShipDate = request.RequestedShipDate
};
}
public Order ToInternalModel(LegacyOrderResponse response)
{
return new Order
{
Id = response.OrderNumber,
CustomerId = response.CustomerKey,
Status = MapStatus(response.OrderStatus),
Items = response.LineItems.Select(ToInternalItem).ToList(),
Total = response.TotalAmount,
CreatedAt = DateTime.Parse(response.CreateDate)
};
}
private OrderStatus MapStatus(string legacyStatus)
{
return legacyStatus switch
{
"OP" => OrderStatus.Open,
"SH" => OrderStatus.Shipped,
"CN" => OrderStatus.Cancelled,
"PD" => OrderStatus.PartiallyDelivered,
_ => OrderStatus.Unknown
};
}
}
ACL with API Management
<!-- APIM Policy for Anti-Corruption -->
<policies>
<inbound>
<!-- Normalize request format -->
<set-header name="X-Legacy-Format" value="JSON" />
<!-- Add correlation -->
<set-variable name="correlationId" value="@(Guid.NewGuid().ToString())" />
<!-- Transform request payload if needed -->
<choose>
<when condition="@(context.Request.Body.ContainsKey("customer_id"))">
<set-body>
@{
var body = context.Request.Body.As<JObject>();
body["customerId"] = body["customer_id"];
body.Remove("customer_id");
return body.ToString();
}
</set-body>
</when>
</choose>
</inbound>
<backend>
<!-- Route to legacy system -->
<set-backend-service base-url="https://legacy-api.company.com" />
<!-- Add legacy-required headers -->
<set-header name="X-Legacy-API-Key" value="{{legacy-api-key}}" />
</backend>
<outbound>
<!-- Transform response to modern format -->
<choose>
<when condition="@(context.Response.StatusCode == 200)">
<!-- Map legacy fields to modern schema -->
<set-body>
@{
var response = context.Response.Body.As<JObject>();
return new JObject
{
["id"] = response["order_id"],
["customer"] = new JObject
{
["id"] = response["cust_num"],
["name"] = response["cust_name"]
},
["status"] = MapLegacyStatus(response["order_status"].ToString()),
["total"] = response["order_total"],
["createdAt"] = response["create_date"]
}.ToString();
}
</set-body>
</when>
</choose>
</outbound>
</policies>
ACL with Logic Apps (Hybrid Integration)
{
"LogicApp": {
"triggers": {
"when-a-request-is-received": {
"method": "POST",
"relativePath": "orders"
}
},
"actions": [
{
"transform_order": {
"type": "DataOperation",
"operation": "Compose",
"inputs": {
"customerId": "@triggerBody()['customer_id']",
"items": "@triggerBody()['line_items']",
"requestedDate": "@addDays(utcNow(), 7)"
}
}
},
{
"validate_legacy": {
"type": "Condition",
"expression": "@greater(length(body('transform_order').items), 0)"
}
},
{
"call_legacy_soap": {
"type": "IntegrationAccount",
"operation": "soap",
"inputs": {
"uri": "https://legacy-soap.company.com/orderservice",
"action": "CreateOrder",
"body": "@body('transform_order')"
}
}
},
{
"map_response": {
"type": "DataOperation",
"operation": "Compose",
"inputs": {
"orderId": "@body('call_legacy_soap')['OrderNumber']",
"status": "Created",
"estimatedDelivery": "@body('call_legacy_soap')['ShipDate']"
}
}
}
]
}
}
Advanced Patterns
Caching Layer
public class CachedAntiCorruptionLayer
{
private readonly IMemoryCache _cache;
private readonly ILegacyClient _legacyClient;
private static readonly TimeSpan DefaultExpiration = TimeSpan.FromMinutes(5);
private static readonly TimeSpan LookupExpiration = TimeSpan.FromHours(1);
public async Task<Customer> GetCustomerAsync(string customerId)
{
var cacheKey = $"customer:{customerId}";
return await _cache.GetOrCreateAsync(cacheKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = LookupExpiration;
var legacyCustomer = await _legacyClient.GetCustomerAsync(customerId);
return _mapper.ToInternalModel(legacyCustomer);
});
}
public async Task InvalidateCustomerAsync(string customerId)
{
// Invalidate on updates to ensure consistency
await _cache.RemoveAsync($"customer:{customerId}");
}
public async Task<List<Order>> GetRecentOrdersAsync(string customerId)
{
// Don't cache mutable data for long
var cacheKey = $"orders:recent:{customerId}";
return await _cache.GetOrCreateAsync(cacheKey, async entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(1);
var orders = await _legacyClient.GetRecentOrdersAsync(customerId);
return orders.Select(_mapper.ToInternalModel).ToList();
});
}
}
Circuit Breaker Integration
public class ResilientAntiCorruptionLayer
{
private readonly ICircuitBreaker _circuitBreaker;
private readonly ILegacyClient _legacyClient;
public async Task<Order> GetOrderWithResilienceAsync(string orderId)
{
// Check circuit breaker state
if (_circuitBreaker.IsOpen)
{
// Return cached or fallback data
return await GetFallbackDataAsync(orderId);
}
try
{
return await _circuitBreaker.ExecuteAsync(() =>
_legacyClient.GetOrderAsync(orderId));
}
catch (Exception ex) when (ex is TimeoutException or ServiceUnavailableException)
{
// Open circuit after repeated failures
_circuitBreaker.RecordFailure();
throw;
}
}
private async Task<Order> GetFallbackDataAsync(string orderId)
{
// Implement fallback: cached data, default values, etc.
var cachedOrder = await _cache.GetAsync<Order>($"fallback:{orderId}");
if (cachedOrder != null)
{
// Mark as potentially stale
cachedOrder.IsStale = true;
return cachedOrder;
}
throw new ServiceUnavailableException(
"Legacy service temporarily unavailable");
}
}
Testing the ACL
Unit Tests for Translation
public class OrderMapperTests
{
private readonly ExternalOrderMapper _mapper;
[Fact]
public void ToLegacyRequest_WithValidInput_ReturnsCorrectFormat()
{
// Arrange
var orderId = "ORD-12345";
// Act
var result = _mapper.ToLegacyRequest(orderId);
// Assert
Assert.Equal("ORD-12345", result.OrderKey);
Assert.Equal("JSON", result.Format);
}
[Fact]
public void ToInternalModel_LegacyResponse_MapsStatusCorrectly()
{
// Arrange
var legacyResponse = new LegacyOrderResponse
{
OrderNumber = "ORD-12345",
OrderStatus = "SH",
OrderTotal = 99.99m
};
// Act
var result = _mapper.ToInternalModel(legacyResponse);
// Assert
Assert.Equal(OrderStatus.Shipped, result.Status);
}
[Theory]
[InlineData("OP", OrderStatus.Open)]
[InlineData("SH", OrderStatus.Shipped)]
[InlineData("CN", OrderStatus.Cancelled)]
[InlineData("PD", OrderStatus.PartiallyDelivered)]
public void ToInternalModel_MapsAllKnownStatuses(string legacyStatus,
OrderStatus expectedStatus)
{
var response = new LegacyOrderResponse { OrderStatus = legacyStatus };
var result = _mapper.ToInternalModel(response);
Assert.Equal(expectedStatus, result.Status);
}
}
Contract Tests
public class LegacyContractTests
{
[Fact]
public async Task GetOrder_HandlesNullResponse_Gracefully()
{
// Simulate legacy returning null
_mockLegacyClient
.Setup(c => c.GetOrderAsync(It.IsAny<string>()))
.ReturnsAsync((LegacyOrderResponse)null);
var acl = new OrderAntiCorruptionLayer(_mockClient.Object);
await Assert.ThrowsAsync<OrderNotFoundException>(() =>
acl.GetOrderAsync("ORD-999"));
}
[Fact]
public async Task GetOrder_HandlesMalformedResponse_ReturnsCleanError()
{
_mockLegacyClient
.Setup(c => c.GetOrderAsync(It.IsAny<string>()))
.ThrowsAsync(new LegacyFormatException("Invalid XML"));
var acl = new OrderAntiCorruptionLayer(_mockClient.Object);
var result = await acl.GetOrderWithErrorHandlingAsync("ORD-123");
Assert.Equal(OrderStatus.Unknown, result.Status);
Assert.Contains("Legacy service error", result.Notes);
}
}
Best Practices
Implementation Checklist
| Practice | Description |
|---|---|
| Single responsibility | ACL only handles translation, not business logic |
| Comprehensive mapping | Handle all fields, including unknown ones |
| Explicit transformations | Don't hide what's being changed |
| Fail gracefully | Handle legacy errors without crashing |
| Test mapping logic | Unit tests for all transformations |
| Version carefully | ACL can become a bottleneck for changes |
Anti-Patterns
┌─────────────────────────────────────────────────────────────────────┐
│ ACL ANTI-PATTERNS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ✗ Adding business logic to ACL (it should just translate) │
│ ✗ Caching without invalidation strategy │
│ ✗ Hiding performance issues from callers │
│ ✗ Making ACL synchronous with all downstream calls │
│ ✗ Not handling legacy errors explicitly │
│ │
└─────────────────────────────────────────────────────────────────────┘
Related Topics
- Strangler Fig Pattern — Legacy migration
- Saga Pattern — Distributed transactions
- API Management Caching — Performance
Azure Integration Hub - Architect Level Enterprise Integration Patterns