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.
Python code
46 linesclass 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
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
- Use a mapper function with `functools.partial` or a `dataclass` for the DTO instead of a custom class
- 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
More from System design patterns
- Build a BFF (Backend for Frontend) Mock Aggregator in Python medium
- Builder pattern for mocking complex objects in Python easy
- Circuit Breaker Pattern in Python: Closed, Open, and Half-Open States medium
- Create a Data Helper Class in Python easy
- Domain Driven Design Aggregate Root Example in Python medium
- Facade Pattern in Python with Mock Simplification medium
Keep learning
Related tutorials and quizzes for this topic.