library-seam-wrapper

Isolate third-party library dependencies behind thin wrapper interfaces to break vendor lock-in and enable testing. Use whenever a developer has direct calls to library classes scattered through production code and can't test or swap the library — 'library is killing me', 'vendor lock-in', 'can't mock this library', 'integration tests only for this SDK', 'AWS SDK everywhere', 'Stripe calls in 50 files', 'all API calls', 'wrapping a library', 'adapter for third-party'. Triggers for 'third party', 'SDK', 'library coupling', 'external service', 'API client'.

Safety Notice

This listing is from the official public ClawHub registry. Review SKILL.md and referenced scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "library-seam-wrapper" with this command: npx skills add quochungto/bookforge-library-seam-wrapper

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:

  1. Library / SDK name and scope — Which library is being wrapped? One class, one module, or the entire SDK surface?
  2. Call-site inventory — Where does the library appear in the codebase? Use Grep to find import statements and class-name usages.
  3. 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.
  4. Language and OO capability — OO languages use the object-seam approach (interface + adapter). C/C++ without viable interfaces fall back to Link Substitution.
  5. 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) not postWithSmtpTransport(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:

  1. Start with the class that has the highest call density or the most urgent test need.
  2. Inject the wrapper interface via constructor parameter (preferred) or a setter.
  3. Verify tests pass after each migrated file before moving to the next.
  4. 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

InputRequiredDescription
Library / SDK nameRequiredThe third-party library to wrap
Codebase accessRequiredSource files containing direct library calls
Call-site inventoryRequiredGrep-generated list of files and usage patterns
Primary goalRequiredTest isolation, vendor swap, or both
LanguageRequiredDetermines 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

  1. "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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

  1. Identify all calls to curl_easy_perform, curl_easy_setopt, and curl_easy_init.
  2. Write a fake_curl.c that provides stub implementations recording calls in memory.
  3. Adjust the test build (Makefile) to link fake_curl.o instead 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:

  1. Identify the domain responsibilities behind the API calls: contact lookup, activity logging, pipeline management.
  2. Create one protocol (Swift) per responsibility: ContactRepository, ActivityLogger, PipelineService.
  3. Implement production adapters that delegate to the CRM SDK.
  4. 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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

Seam Type Selector

Select the right seam type (Preprocessor / Link / Object) for breaking a dependency in legacy code. Use whenever a developer needs to substitute behavior for...

Registry SourceRecently Updated
00Profile unavailable
Coding

Monster Method Decomposition

Decompose a very large method (100+ lines, deeply nested) safely using automated refactoring and Feathers' Bulleted/Snarled classification. Use whenever a de...

Registry SourceRecently Updated
00Profile unavailable
Coding

Legacy Code Addition Techniques

Add new functionality to untested legacy code using Sprout Method, Sprout Class, Wrap Method, or Wrap Class — whichever best fits the dependency profile. Use...

Registry SourceRecently Updated
00Profile unavailable
Coding

Dependency Breaking Technique Executor

Select and execute the right dependency-breaking technique from Michael Feathers' catalog of 24 named techniques (Part III of Working Effectively with Legacy...

Registry SourceRecently Updated
00Profile unavailable