Install
openclaw skills install bookforge-library-seam-wrapperIsolate 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'.
openclaw skills install bookforge-library-seam-wrapperUse 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:
Do not use for in-house or fully-controlled libraries — those can be changed or extended directly without wrapping.
Before starting, establish:
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:
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.
Choose between two strategies based on API complexity and migration risk:
Skin and Wrap (preferred when feasible):
Responsibility-Based Extraction (when the API is large or tangled):
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."
The interface must reflect your use case, not the library's API:
sendTransactionalEmail(recipient, subject, body) not postWithSmtpTransport(SmtpSession, InternetAddress[], MimeMessage).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."
Create a class that:
The adapter is thin by design. If business logic creeps in, extract it to the calling class instead.
Replace direct library calls with wrapper calls one file at a time, not all at once:
Incremental migration avoids the "big bang" refactor that breaks the entire build simultaneously.
With the interface in place, write a Fake<ServiceName> class that:
Tests inject the fake; production code uses the real adapter. The interface is the only contract both must satisfy.
| 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 |
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]
The implementing class, reviewed for zero business logic and single responsibility.
Ordered list of call sites to migrate, one per file, with estimated test coverage gain per step.
"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.
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.
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:
curl_easy_perform, curl_easy_setopt, and curl_easy_init.fake_curl.c that provides stub implementations recording calls in memory.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.
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:
ContactRepository, ActivityLogger, PipelineService.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.
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.
CC-BY-SA-4.0 — derived from Working Effectively with Legacy Code by Michael C. Feathers (2004).
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.