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

PracticeDescription
Single responsibilityACL only handles translation, not business logic
Comprehensive mappingHandle all fields, including unknown ones
Explicit transformationsDon't hide what's being changed
Fail gracefullyHandle legacy errors without crashing
Test mapping logicUnit tests for all transformations
Version carefullyACL 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


Azure Integration Hub - Architect Level Enterprise Integration Patterns