How to Build an Anti-Corruption Layer in Python

Wrap a legacy system with a translation layer that converts awkward legacy data into a clean, modern DTO (Data Transfer Object) for use by new code.

Medium Python 3.9+ Aug 9, 2026 System design patterns 15 views 0 copies

Python code

46 lines
Python 3.9+
class LegacyOrderSystem:
    """Legacy system with awkward, unstructured data."""
    def get_order(self):
        return {
            "order_id": "ORD-123",
            "cust": "Acme Corp",
            "items": [{"sku": "A1", "qty": 2, "price_each": 10.0}],
            "ship_to": "123 Main St, Springfield"
        }


class OrderDataTransferObject:
    """Modern clean DTO used by the new system."""
    def __init__(self, order_id, customer, items, shipping_address):
        self.order_id = order_id
        self.customer = customer
        self.items = items
        self.shipping_address = shipping_address

    def __repr__(self):
        return (f"OrderDTO(id={self.order_id!r}, customer={self.customer!r}, "
                f"items={self.items!r}, address={self.shipping_address!r})")


class AntiCorruptionLayer:
    """Translates legacy format into modern DTO."""

    def __init__(self, legacy_system):
        self.legacy = legacy_system

    def get_modern_order(self):
        raw = self.legacy.get_order()
        return OrderDataTransferObject(
            order_id=raw["order_id"].replace("ORD-", ""),  # strip prefix
            customer=raw["cust"].upper(),  # normalize casing
            items=[(item["sku"], item["qty"], item["price_each"])
                   for item in raw["items"]],
            shipping_address=raw["ship_to"]
        )


if __name__ == "__main__":
    legacy = LegacyOrderSystem()
    translator = AntiCorruptionLayer(legacy)
    dto = translator.get_modern_order()
    print(dto)

Output

stdout
OrderDTO(id='123', customer='ACME CORP', items=[('A1', 2, 10.0)], address='123 Main St, Springfield')

How it works

The Anti-Corruption Layer (ACL) shields your new system from legacy data formats by wrapping the legacy call and translating its output. Here, get_modern_order pulls raw dict data from LegacyOrderSystem, then maps fields into OrderDataTransferObject with clean names and normalized values — stripping the 'ORD-' prefix, uppercasing the customer, and converting items to tuples. This layering means the new codebase never sees legacy quirks, so changes to the legacy system (or a future migration) only touch the ACL, not all consumers. The pattern keeps boundaries clean and is a classic domain-driven design practice for incremental system replacement.

Common mistakes

  • Putting translation logic inside the new domain models, which couples them to legacy quirks
  • Not isolating the legacy call behind an interface, so callers depend directly on the legacy API
  • Mutating the legacy raw data in place instead of building a fresh DTO

Variations

  1. Use a mapper function with `functools.partial` or a `dataclass` for the DTO instead of a custom class
  2. Add field-level validators in the ACL to raise clear errors on unexpected legacy data

Real-world use cases

  • Migrating a monolith to microservices — wrap the old order API so new services consume clean data.
  • Integrating a third-party ERP with quirky field names, normalizing them before persisting.
  • Supporting multiple legacy vendors in one pipeline by giving each its own translator behind a common interface.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from System design patterns

Related tutorials and quizzes for this topic.