skip to content

When is it worth putting your own interface (for example an anti-corruption layer) in front of a third-party library or external service, and when is that wrapper just ceremony?

level: seniorimportance: nice to knowfreq 30%

answer

  1. wrap for volatility, vocabulary, testability, cross-cutting policy, blast radius
  2. anti-corruption layer = translate their model into yours (DDD)
  3. don't wrap stable ubiquitous standards
  4. wrapper must express your need, not mirror their API
  5. fakes need contract tests against the real adapter

basics

~20 s

Wrap when the dependency is likely to change, imposes its types on your core domain, or is hard to test against. Skip the wrapper when the dependency is stable, ubiquitous and already speaks a good vocabulary — a wrapper that mirrors its API buys nothing.

solid answer

~60 s

Wrap when at least one holds: - **Volatility** — the vendor, protocol or SDK is likely to change, or you are mid-migration and want a strangler seam. - **Vocabulary mismatch** — the library's model would otherwise infect your domain (an anti-corruption layer in Domain-Driven Design translates their model into yours). - **Testability** — the real thing needs network, credentials or a container; a narrow port lets you use fast fakes and keeps integration tests to the adapter. - **Cross-cutting policy** — you need retries, timeouts, metrics, redaction or authorisation applied uniformly at one place. - **Blast radius** — the dependency appears in hundreds of call sites, so a future change would otherwise be a mass edit. Skip it when the library is stable and ubiquitous (a logging facade, a collections or date-time library, the standard HTTP client) — you would be re-inventing a worse API and adding a shallow layer. Key rule: the wrapper must express **your** need, not mirror theirs. A wrapper with a method per vendor call is a shallow layer; a wrapper exposing `charge(order)` or `publish(event)` is a real boundary.

code

pseudocode · 7 lines
pseudocode
// Ceremony: mirrors the vendor, leaks their types and errors
interface StripeGateway { createCharge(StripeChargeRequest): StripeCharge }

// Real boundary: your vocabulary, your errors, narrow surface
interface Payments {
  charge(orderId, Money): Result<PaymentId, PaymentFailure>  // Declined | Retryable | Invalid
}

go deeper

for a junior

Say you wrap when the library is likely to change or is hard to test against, and that a wrapper copying the library's methods adds nothing.

for a middle

List concrete triggers (volatility, testability, cross-cutting concerns, narrowing surface) and the counter-cases (stable ubiquitous libraries).

for a senior

Name the anti-corruption layer and ports-and-adapters framing, insist the wrapper own vocabulary and failure taxonomy, and raise fake drift plus contract tests.

for a principal

Make it policy: which categories of dependency must be behind a boundary (payments, PII processors, vendor-specific infrastructure), enforce confinement with architecture tests instead of blanket wrapping, and weigh supply-chain, licence and exit risk alongside engineering cost.

## The decision, framed Adding your own interface in front of a dependency is exactly the 'add a level of indirection' move — so it should be judged by the usual cost/benefit: what change does it make cheap, and what does it cost every reader? ## Reasons that justify a wrapper **1. Volatility / expected substitution.** You are actively migrating (strangler fig), you have a second vendor contracted, or the SDK has a history of breaking changes. Here the option being bought is real and dated. **2. Model protection (anti-corruption layer).** In Domain-Driven Design terms, an ACL translates an external bounded context's model into yours so their concepts do not leak into your core. Symptom that you need one: vendor types appearing in domain method signatures, or their error taxonomy dictating your control flow. **3. Testability.** If exercising the dependency needs network access, credentials, a container or paid calls, a narrow port lets the bulk of your tests run against an in-memory fake, with a small set of contract/integration tests pinning the adapter. This is often the single strongest practical reason. **4. Uniform cross-cutting behaviour.** Timeouts, retry/backoff, circuit breaking, metrics, tracing, PII redaction, tenancy checks — enforcing these once at a boundary is much more reliable than at 200 call sites. **5. Narrowing surface.** You use 4 of the SDK's 300 operations; exposing only those makes the rest unavailable by construction and documents actual usage. **6. Blast radius / licence and compliance risk.** A dependency embedded in hundreds of files is effectively unremovable; a boundary keeps removal a bounded project. ## Reasons to skip it - **Ubiquitous, stable, standard.** Collections, date/time, JSON, standard HTTP client, a logging facade that is already an abstraction. Wrapping adds a name nobody knows for something everyone knows. - **The wrapper would mirror the API 1:1.** That is indirection without abstraction — a shallow module. - **You cannot actually swap it.** If the dependency's semantics are unique (a specific database's transactional/indexing behaviour), a thin interface creates the *illusion* of portability while real behaviour still leaks through — arguably worse than an honest direct dependency, because it hides the coupling. - **You will guess the seam wrong.** Interfaces designed before you understand your usage tend to encode the vendor's shape anyway. Extracting later, once usage is known, is a mechanical refactor. ## Designing the wrapper well, when you build one - **Own the vocabulary and the types.** Inputs and outputs are your domain types, not vendor DTOs; vendor errors are translated into your error taxonomy. - **Own the failure model.** Do not let vendor exceptions escape; classify into retryable/permanent/invalid. - **Keep it deep.** A few capability-shaped operations, not a method per vendor endpoint. - **Be honest about leaks.** Remote latency and partial failure must remain visible in the contract; do not present a network call as if it were local. - **Verify the fake.** If tests run against an in-memory implementation, protect yourself with contract tests that run the same suite against the real adapter, otherwise the fake drifts and green tests mean nothing. - **Keep exactly one adapter per dependency**, in an outer layer (ports-and-adapters / hexagonal architecture), with the port owned by the domain side. ## Middle grounds worth knowing - Wrap **only the risky part** (e.g. wrap payment authorisation, use the vendor's plain data types elsewhere). - Wrap **at the seam you actually need**, e.g. a `Clock` or `IdGenerator` for testability rather than a full SDK facade. - Use the vendor SDK directly but confine it to one package with an architecture rule (dependency-cruiser, ArchUnit, module boundaries) forbidding imports elsewhere — much of the benefit at a fraction of the cost.

  • If you skip the wrapper now, how do you keep the option of adding it later cheap?
    Confine the dependency's imports to one package or module and enforce that with an architecture test (ArchUnit, dependency-cruiser, module boundary rules). Usage stays direct and readable, but the future extraction is a bounded refactor in one place rather than a search across the codebase.
  • What is the risk of testing everything against an in-memory fake behind your port?
    Fake drift: the fake accepts inputs the real service rejects, or has different ordering, latency and failure semantics, so tests stay green while production breaks. Mitigate with contract tests — one suite executed against both the fake and the real adapter — plus a small set of integration tests on the adapter itself.
  • Is 'we might swap databases' a good reason to put an interface over data access?
    Rarely on its own: real portability is defeated by semantic differences (transactions, indexing, consistency, SQL dialect) that leak through any thin interface, and the illusion of portability can be worse than an honest dependency. Better justifications are testability, uniform policy, and keeping persistence vocabulary out of the domain.

An embassy translates and filters everything that crosses the border in your own terms. A revolving door just makes you walk through the same doorway more slowly.

saying these in an interview costs you the question

  • Wrapping every third-party library on principle
  • Building a wrapper whose methods mirror the vendor API one-for-one
  • Letting vendor types or vendor exceptions escape through your own interface
  • Claiming database portability from a thin repository interface
  • Relying on a hand-written fake with no contract test against the real implementation

context