Agent Wranglers

ARC-77Recommended

Translate outside models at the boundary

Convert the types of every outside system into your own types in one adapter module. Examples are a payment provider, a legacy app or another context. No other code imports the outside types.

How to do it

This is called an anti-corruption layer. Without it, a provider's field names and status codes spread through the code, and a new API version breaks every file that used them. The adapter maps the outside shape to your own type and your own words. It is the place to parse and check that input, as Parse input once at the boundary says. It also lets a strangler migration swap the old system for a new one behind the same types.

Examples

TypeScript example

// src/billing/adapters/stripe.ts is the only file that imports 'stripe'
import type Stripe from 'stripe';
import { Subscription, SubscriptionStatus } from '../subscription';

const STATUS: Record<Stripe.Subscription.Status, SubscriptionStatus> = {
  active: 'active', trialing: 'active', past_due: 'grace', unpaid: 'ended',
  canceled: 'ended', incomplete: 'pending', incomplete_expired: 'ended', paused: 'paused',
};

export const toSubscription = (s: Stripe.Subscription): Subscription =>
  new Subscription(s.id, STATUS[s.status], new Date(s.items.data[0].current_period_end * 1000));

Python example

# billing/adapters/stripe.py is the only module that imports stripe
from datetime import UTC, datetime
from billing.subscription import Subscription, SubscriptionStatus

STATUS = {
    "active": SubscriptionStatus.ACTIVE, "trialing": SubscriptionStatus.ACTIVE,
    "past_due": SubscriptionStatus.GRACE, "unpaid": SubscriptionStatus.ENDED,
    "canceled": SubscriptionStatus.ENDED, "incomplete": SubscriptionStatus.PENDING,
    "incomplete_expired": SubscriptionStatus.ENDED, "paused": SubscriptionStatus.PAUSED,
}

def to_subscription(s) -> Subscription:
    period_end = datetime.fromtimestamp(s["items"]["data"][0]["current_period_end"], UTC)
    return Subscription(s["id"], STATUS[s["status"]], period_end)

How to check

An import rule fails the build when a file outside the adapter module imports the outside SDK. Examples are no-restricted-imports in ESLint and the banned-api rule in Ruff.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/architecture/domain-model/anti-corruption-layer">Agent Wranglers clean code and architecture playbook</a>. In code you write, add a comment next to it with the same link.