Rough Work/the craft

Wrappers Are Design Tools

You integrate a payment library. Almost immediately, its terminology starts appearing in your business logic. Your clean "Order" code now talks about amount * 100, currency, source, and metadata. External jargon has leaked into the core of your application:

// Without wrapper: External concepts leak into domain logic
async function completeOrder(order: Order) {
  await stripe.charges.create({
    amount: order.total * 100,
    currency: 'usd',
    source: token,
    metadata: { order_id: order.id }
  });

  order.status = 'paid';
}

A wrapper translates between your domain and theirs. It lets the rest of your code speak your language:

// With wrapper: Domain logic stays clean
async function completeOrder(order: Order) {
  await payments.processOrder(order);
  order.status = 'paid';
}

// Wrapper isolates vendor complexity
class StripePaymentService {
  async processOrder(order: Order) {
    await stripe.charges.create({
      amount: order.total * 100,
      currency: 'usd',
      source: this.getToken(),
      metadata: { order_id: order.id }
    });
  }
}

At the boundary, your code says payments.processOrder(order). Inside the wrapper, you handle the vendor details — the data transformation, the authentication, the error codes. Your application talks about orders, customers, and payments. Concepts from your domain, not someone else's SDK.

The pattern applies anywhere you cross an external boundary — a PDF generator, an email service, a third-party API. When the library changes, you update the wrapper. The rest of the application doesn't know anything changed. A wrapper isn't just an adapter — it's a language barrier that protects your domain from the complexity of the outside world.

to navigate