How should you manage the boundary between your code and a third-party library or external API, and what are "learning tests" in that context?
answer
- Wrap the vendor; own the interface
- Adapter/ACL at the seam, domain vocabulary inside
- Learning tests = executable knowledge of real behaviour
- Learning tests fail on upgrade, before production does
- Narrow the API; a pass-through wrapper buys nothing
basics
~20 sDo not let a third-party type spread through your codebase. Wrap it behind an interface you own, expressed in your domain's terms, and keep the vendor types inside that wrapper. Learning tests are small tests you write against the library to verify how it actually behaves - and they warn you when a version upgrade changes that behaviour.
solid answer
~60 sThird-party code is designed for its authors' generality, not your use case, and it changes on its own schedule. So define the seam: an **interface you own**, named in your domain vocabulary, with an **adapter/wrapper** as the only place the vendor types, exceptions, and config appear. Benefits: a version upgrade or vendor swap has one blast radius; you expose only the subset you need (a `Sensors` interface, not a raw `Map<String,Object>`); vendor exceptions are translated into your error model; and tests can substitute a fake instead of the real client. **Learning tests** are throwaway-turned-permanent tests you write *against the library itself* to discover its real behaviour - what it does with an empty input, whether it throws or returns null, how it handles time zones. You pay the learning cost anyway; capturing it as tests makes the knowledge executable. Kept in CI, they become a regression suite that fails on upgrade when the library's behaviour drifts. Caveats: do not wrap everything reflexively - a wrapper over a stable, ubiquitous standard adds indirection for nothing - and avoid the pass-through wrapper that merely renames the vendor API without changing its shape.
code
pseudocode · 13 lines// Interface you own, in your vocabulary
interface PaymentGateway { charge(Money amount, CardToken token) -> ChargeResult }
// Adapter: the ONLY file that imports the vendor SDK
class StripeGateway implements PaymentGateway {
charge(amount, token) {
try { return toResult(stripe.charges.create(...)) }
catch (StripeException e) { throw PaymentFailed(translate(e)) }
}
}
// Learning test: what does the SDK actually do on a declined card?
test "declined card raises CardError with code card_declined" { ... }go deeper
Say: keep the library in one place behind your own interface, and write small tests that check how the library really behaves.
Add the adapter/wrapper mechanics - narrowing the API, translating exceptions, injecting a fake in tests - and why learning tests catch upgrade breakage.
Cover interface-first design when the dependency does not exist yet, anti-corruption layers, what to wrap and what not to, and leaky semantics you must surface deliberately.
Frame it as dependency risk management: blast radius on vendor change, versioning and contract testing strategy, one owning module per external system, and organisational rules that keep vendor imports out of the domain.
## The problem with boundaries A third-party library, SDK, or external service is written to satisfy many users. Its API is broader than your need, its error model is its own, and its release schedule is not yours. If its types leak everywhere - imported in a hundred files, its exceptions caught in your controllers, its config objects passed through your domain - then the library has effectively become part of your architecture, and you upgrade or replace it only by touching everything. ## Techniques **1. Wrap it.** Put the vendor behind a small class you own. The wrapper's public surface is *your* vocabulary and *your* subset of the functionality. Classic Clean Code example: rather than passing a generic `Map<String, Sensor>` around (a type whose full, mutable, untyped API is exposed to every caller), define a `Sensors` class with `getById(id)` and keep the map private. The general-purpose type is narrowed to a purposeful one. **2. Define the interface from your side.** Especially when the third party does not exist yet, or you cannot call it in tests: write the interface your code *wishes* it had, code against it, and implement the adapter later. This keeps design pressure on your needs rather than the vendor's shape. **3. Adapter pattern at the seam.** The adapter translates in both directions: your domain objects in, vendor requests out; vendor responses and errors in, your domain types and error model out. Timeouts, retries, auth, serialization quirks, pagination - all vendor concerns stay inside. **4. Anti-corruption layer (ACL).** The Domain-Driven Design name for the same idea at a larger scale: a translation layer between your bounded context and a foreign model (a legacy system, another team's service) so their concepts never contaminate yours. **5. Learning tests.** Instead of only reading documentation, write small tests that call the library and assert what you *believe* it does. Concretely, for a logging library: assert what a basic logger prints; for a date library: assert what it returns for a leap day or an ambiguous DST time; for an HTTP client: assert its behaviour on a 500 or a timeout. Three payoffs: - You learn faster, with feedback, and in a sandbox rather than in production code. - The tests document the exact behaviour you depend on. - **On upgrade they run in CI.** If version N+1 changes semantics, the learning tests fail *before* your production code does, at a place that tells you exactly what changed. The library then effectively has a contract test suite it never shipped. ## Trade-offs and failure modes - **Do not wrap everything.** Wrapping the language's own collections or a ubiquitous, stable standard buys indirection and costs readability. Wrap where the risk is real: volatile APIs, vendors you might replace, awkward APIs, anything hard to test. - **Pass-through wrappers are worthless.** If your interface has one method per vendor method with the same parameters and the vendor's exception types leaking through, you renamed the coupling instead of removing it. A good wrapper is *narrower* and speaks your language. - **Leaky abstractions.** Some vendor semantics cannot be hidden - connection pooling behaviour, eventual consistency, rate-limit backpressure. Decide deliberately what to expose in your own terms rather than pretending it does not exist. - **Test doubles you own.** Because the interface is yours, you can write an in-memory fake and test your code without the network. Mocking the vendor's own types directly is fragile and re-couples your tests to their API. - **Contract tests / integration tests still needed.** Learning tests verify the library in isolation; you still need at least one integration test proving the adapter wires up correctly against the real thing (or a sanctioned test container/sandbox). ## Rule of thumb Code that *uses* the boundary should read as if the third party does not exist. Exactly one module should know its name.
- Should every third-party library be wrapped?No. Wrap where the risk is real - volatile APIs, replaceable vendors, awkward or hard-to-test surfaces, anything whose types would otherwise spread widely. Wrapping ubiquitous stable standards adds indirection without reducing risk.
- How do learning tests differ from the library's own unit tests, and who runs them?They are yours, they assert only the behaviour you depend on, and they live in your CI. The library's tests protect the library's whole surface on its schedule; your learning tests fail at your upgrade, pinpointing the exact semantic change that affects you.
- How do you test code that depends on the boundary without hitting the network?Because the interface is yours, write an in-memory fake implementation and inject it. Reserve real calls for a small number of integration/contract tests against a sandbox or test container.
An embassy: everything foreign passes through one building that translates law, language, and currency. Citizens inside never negotiate with the foreign legal system directly.
saying these in an interview costs you the question
- Letting vendor types and exceptions appear throughout the domain and controllers
- Building a one-to-one pass-through wrapper and calling it decoupling
- Wrapping absolutely everything, including standard library types
- Treating learning tests as throwaway instead of keeping them in CI as upgrade tripwires
- Mocking the third-party classes directly in unit tests instead of faking your own interface
- Assuming a wrapper makes the vendor swappable when its semantics (consistency, backpressure, auth model) leak through anyway