Library Seam Wrapper
When to Use
Use this skill when production code calls third-party library or SDK classes directly — and those calls are making testing hard or locking the team into a vendor.
Concrete triggers:
- Tests require the real library (slow, expensive, flaky) because there is no seam to inject a fake.
- A vendor changed pricing, deprecated an API, or introduced licensing terms the team cannot meet, and switching would require touching dozens of files.
- The codebase is "mostly API calls" — every class is a thin shell around external service SDKs with no testable seam.
- A developer says "we'll never need to change this library" — Feathers' warning: that belief can become a self-fulfilling prophecy.
Do not use for in-house or fully-controlled libraries — those can be changed or extended directly without wrapping.
Context and Input Gathering
Before starting, establish:
- Library / SDK name and scope — Which library is being wrapped? One class, one module, or the entire SDK surface?
- Call-site inventory — Where does the library appear in the codebase? Use Grep to find import statements and class-name usages.
- Primary goal — Is the goal to enable test fakes, or to hedge against vendor swap, or both? The goal influences how domain-expressive the wrapper interface needs to be.
- Language and OO capability — OO languages use the object-seam approach (interface + adapter). C/C++ without viable interfaces fall back to Link Substitution.
- API complexity — A small, stable API is a good candidate for full Skin and Wrap. A large, complicated API is a better candidate for Responsibility-Based Extraction.
Process
Step 1: Inventory direct library calls
Grep the codebase for all import statements and direct usage of the library's class names. Build a call-site map:
Grep pattern: import.*<LibraryPackage> OR new <LibraryClass>( OR <LibraryClass>.
Record:
- Which files import the library
- Which class or method names from the library appear in production code
- Whether usages cluster in a few classes or are scattered across the codebase
A scattered inventory confirms the anti-pattern Feathers names: the library has become structurally embedded, and every call site is a seam that does not exist.
Step 2: Classify wrapping scope
Choose between two strategies based on API complexity and migration risk:
Skin and Wrap (preferred when feasible):
- Create an interface that mirrors the library's surface you actually use — not its entire API, only the methods your code calls.
- Implement a production adapter that delegates to the real library.
- Production code depends only on the interface; zero library imports outside the adapter.
- Result: complete isolation. The adapter is the only file that touches the library; swapping vendors means writing one new adapter.
- Good when: the API is relatively small, the team wants total isolation, or there are no tests and the only safe path is wrapping first.
Responsibility-Based Extraction (when the API is large or tangled):
- Identify the domain responsibility behind each cluster of API calls (e.g., "send a message", "check for mail").
- Extract those responsibilities into methods on a new class, giving them domain-expressive names.
- The new class still calls the library, but the consuming code depends on higher-level behavior, not raw API calls.
- Result: partial isolation. The extracted class is still testable through its interface; some API coupling may remain inside it.
- Good when: the API is complicated, refactoring tools are available, or an all-at-once rewrite is not safe.
Many teams use both: a thin wrapper for test isolation, and a higher-level responsibility wrapper to give the application a domain language. Feathers: "Skin and wrap is more work, but it can be very useful when we want to isolate ourselves from third-party libraries."
Step 3: Design the wrapper interface
The interface must reflect your use case, not the library's API:
- Name methods by what they do for your domain:
sendTransactionalEmail(recipient, subject, body)notpostWithSmtpTransport(SmtpSession, InternetAddress[], MimeMessage). - Include only the methods your production code actually calls — do not mirror the whole library.
- Keep the interface stable against library changes; it should be driven by your domain needs.
- One interface per logical service (payment, messaging, storage) keeps fakes small and readable.
If the existing code directly instantiates library objects (e.g., new Transport()) that cannot be subclassed — because the library class is final, sealed, or has non-virtual methods — wrapping is the only viable option. Feathers notes: "Sometimes wrapping the singleton is the only choice available to you."
Step 4: Implement the production adapter
Create a class that:
- Implements the wrapper interface.
- Holds a reference to the real library object (injected or created internally).
- Delegates each interface method to the library — no business logic inside the adapter.
- Is the only file in the codebase that imports the library package.
The adapter is thin by design. If business logic creeps in, extract it to the calling class instead.
Step 5: Migrate call sites incrementally
Replace direct library calls with wrapper calls one file at a time, not all at once:
- Start with the class that has the highest call density or the most urgent test need.
- Inject the wrapper interface via constructor parameter (preferred) or a setter.
- Verify tests pass after each migrated file before moving to the next.
- Leave the remaining call sites temporarily; the build stays green throughout.
Incremental migration avoids the "big bang" refactor that breaks the entire build simultaneously.
Step 6: Create a fake implementation for tests
With the interface in place, write a Fake<ServiceName> class that:
- Implements the wrapper interface.
- Records calls (captures arguments) or returns scripted responses.
- Contains no real I/O or network calls.
- Lives in the test source tree only.
Tests inject the fake; production code uses the real adapter. The interface is the only contract both must satisfy.
Inputs
| Input | Required | Description |
|---|---|---|
| Library / SDK name | Required | The third-party library to wrap |
| Codebase access | Required | Source files containing direct library calls |
| Call-site inventory | Required | Grep-generated list of files and usage patterns |
| Primary goal | Required | Test isolation, vendor swap, or both |
| Language | Required | Determines whether object seam or Link Substitution applies |
Outputs
wrapper-design.md
Documents the wrapping decision for the team:
## Wrapper Design: [Library Name]
**Strategy:** [Skin and Wrap | Responsibility-Based Extraction | Both]
**Rationale:** [1–2 sentences on why this strategy for this API]
**Interface:** [InterfaceName]
**Methods:**
- `methodName(params): ReturnType` — [what it does for the domain]
**Production Adapter:** [AdapterClassName]
**Fake Implementation:** [FakeClassName] (test source only)
**Enabling Point:** [Where the adapter is injected — constructor param, factory, etc.]
**Migration plan:** [List of files to migrate, in order of priority]
Production adapter source file
The implementing class, reviewed for zero business logic and single responsibility.
migration-plan.md
Ordered list of call sites to migrate, one per file, with estimated test coverage gain per step.
Key Principles
-
"We'll never change this library" is a self-fulfilling prophecy. Every hard-coded library call is a seam that does not exist. The team cannot fake it, cannot swap the vendor, and eventually cannot change it at all. Wrap before the cost becomes prohibitive.
-
The wrapper interface names the use case, not the library API.
PaymentGateway.charge(amount, currency, token)is a domain concept.StripeClient.createPaymentIntent(PaymentIntentCreateParams)is a library detail. The interface protects consuming code from library churn. -
Full wrap is better than skin-and-wrap, but skin-and-wrap beats no wrap. Even a thin pass-through interface that mirrors the library API provides the test-injection seam. Improve the domain naming later.
-
A library-using class can wrap itself. If a class uses a library for a single responsibility, extract that responsibility into an interface on the class itself. The class becomes its own adapter; no new file required.
-
The adapter is the only file that imports the library. Enforcing this as a convention (via linting or package visibility) prevents the anti-pattern from re-emerging after migration.
Examples
Example A: Stripe SDK scattered across 40 Java files
Situation: A payment service has StripeClient, Charge, and PaymentIntent imports in 40 files. Unit tests hit the real Stripe API with test keys — they are slow and occasionally fail due to network timeouts.
Strategy: Skin and Wrap. The Stripe API surface actually used is small: create a charge, retrieve a charge, refund.
Interface:
interface PaymentGateway {
ChargeResult charge(Money amount, String token);
ChargeResult retrieve(String chargeId);
RefundResult refund(String chargeId, Money amount);
}
Production adapter: StripePaymentGateway implements PaymentGateway — delegates to StripeClient, zero business logic.
Fake: FakePaymentGateway implements PaymentGateway — stores charges in a List, returns scripted responses.
Migration: Inject PaymentGateway via constructor into each service class, replacing direct StripeClient usages one class at a time.
Outcome: 40 files see the interface only. Switching to Braintree means writing one new adapter.
Example C: C codebase with curl calls (Link Substitution fallback)
Situation: A C codebase makes HTTP calls via curl_easy_perform() in 30 files. No OO structure exists; an interface cannot be defined. The team needs to test HTTP-dependent functions without real network calls.
Strategy: Link Substitution (Ch 25 fallback for procedural languages when interface wrapping is not feasible).
Steps:
- Identify all calls to
curl_easy_perform,curl_easy_setopt, andcurl_easy_init. - Write a
fake_curl.cthat provides stub implementations recording calls in memory. - Adjust the test build (Makefile) to link
fake_curl.oinstead of the real libcurl.
The production Makefile links the real libcurl. The test Makefile links the fake. Source files are untouched.
Limitation: Link Substitution provides fake isolation but not vendor-swap isolation. A full wrapper in OO code is preferable when feasible.
Example C: iOS app that is 90% CRM API calls
Situation: An iOS CRM client has view controllers calling the vendor SDK directly. 90% of the code is mapping API responses to UI. A product manager wants to evaluate switching vendors.
Strategy: Responsibility-Based Extraction with per-service wrappers.
Decomposition:
- Identify the domain responsibilities behind the API calls: contact lookup, activity logging, pipeline management.
- Create one protocol (Swift) per responsibility:
ContactRepository,ActivityLogger,PipelineService. - Implement production adapters that delegate to the CRM SDK.
- View controllers depend only on the protocols.
Outcome: Evaluating a new vendor requires writing three new adapters, not auditing 200 view controller methods. The app's logic is now testable without the CRM SDK.
References
See references/wrapper-design-template.md for a blank wrapper-design.md template and a decision checklist for choosing between Skin and Wrap and Responsibility-Based Extraction.
License
CC-BY-SA-4.0 — derived from Working Effectively with Legacy Code by Michael C. Feathers (2004).
Related BookForge Skills
seam-type-selector— Prerequisite. Library wrapping is the object-seam approach; this skill helps you confirm it before investing in wrapper design.dependency-breaking-technique-executor— Extract Interface is the primary mechanic behind Step 3 and Step 4 of this skill. Use it when executing the interface extraction in a specific language.legacy-code-symptom-router— Routes legacy code problems to the right skill. If the symptom is "can't test because of a library", it should direct here.