In legacy-code refactoring, what is a "seam" and its "enabling point", and what are the main kinds of seam available (object, link, and preprocessing/build seams)?
answer
- Change behaviour without editing there
- Enabling point = where you choose
- Object / link / preprocessing
- Statics have no object seam
- Prefer object seams; link/preproc are escapes
basics
~20 sA seam is a place where you can change what code does without editing that code in that place. Its enabling point is where you make the choice (a constructor argument, a build configuration, a linker path). Seams let you swap real collaborators for fakes so untestable code becomes testable.
solid answer
~50 sA seam is a point where behaviour can be substituted without editing the code at that point; every seam has an enabling point where the substitution decision is made. Object seams are the workhorse: a call goes through a polymorphic reference (interface, virtual method, function value), so passing a different implementation via constructor or setter changes behaviour — the enabling point is that injection site, not the call site. Link seams substitute at build/link time: a different library, jar, stub object file, or module resolution path replaces the real implementation; enabling point is the build or classpath configuration. Preprocessing/build seams use compile-time substitution such as C/C++ macros, conditional compilation, or bytecode/source generation; enabling point is the build flag. Prefer object seams: they are visible in the source, do not require a special build, and push the design toward dependency inversion. Link and preprocessing seams exist for languages or situations where you cannot introduce polymorphism — static/free functions, third-party code you cannot edit, or system calls.
code
pseudocode · 15 lines// NO seam: hard-wired construction, nothing to substitute
class Reminder {
send(user) { new SmtpMailer().mail(user.email, body()) }
}
// Object seam created by Extract-and-Override Factory Method
class Reminder {
protected mailer() { return new SmtpMailer() } // ENABLING POINT (override in test)
send(user) { mailer().mail(user.email, body()) } // SEAM (call dispatches polymorphically)
}
class TestableReminder extends Reminder {
sent = []
protected mailer() { return { mail: (to, b) => sent.push([to, b]) } }
}go deeper
Define a seam as a place where behaviour can be changed without editing there, give the constructor-injection example, and name the enabling point.
Distinguish object, link and preprocessing seams with one concrete example each, and name the refactorings that create object seams (Extract Interface, Extract and Override Factory Method, Subclass and Override Method).
Reason about selection: when statics/third-party binaries force link or preprocessing seams, how sensing differs from separation, and why creating a real seam beats a bytecode-rewriting mock.
Discuss seams as an architectural property — systematically placing them at process, clock, filesystem, and network boundaries to reduce flakiness and enable testing at scale, and the governance cost of build-time substitution reaching production.
## Definition Feathers: **a seam is a place where you can alter behaviour in your program without editing in that place.** Every seam has an **enabling point** — the place where you can decide which behaviour is used. The two halves matter equally. If you can substitute behaviour only by editing the very line that does the work, there is no seam — you are just modifying code, which is exactly the risky act you were trying to avoid. If there is no enabling point reachable from a test, the theoretical substitution is useless. Why this matters: getting legacy code under test almost always means preventing something (a database call, a network hop, a clock read, a payment charge) from happening for real, or observing something that normally disappears. Seams are the inventory of places where that is possible. ## Object seams A call site that dispatches through a polymorphic reference is an object seam. ``` class OrderProcessor { constructor(gateway) { this.gateway = gateway } // <-- ENABLING POINT process(order) { this.gateway.charge(order.total()) // <-- SEAM } } ``` The call `gateway.charge(...)` is the seam: you can change what happens there without editing that line. The enabling point is the constructor parameter, because that is where you choose the implementation. In a test you pass a fake gateway that records the amount instead of charging a card. Common enabling points for object seams: - Constructor parameter (best: the object is never in a wrong state). - Setter or property (works, but allows a half-configured object). - Factory method that the test subclasses and overrides (**Extract and Override Factory Method**) — useful when the class currently `new`s its collaborator inline and you want a minimal, signature-preserving change. - Overriding the calling method itself in a test subclass (**Subclass and Override Method**), a fast way to neutralize an inconvenient call. - Passing a function/lambda instead of an object, in languages with first-class functions. A static call (`PaymentGateway.charge(...)`, `DateTime.now()`, `Logger.getInstance()`) is *not* an object seam: there is no reference to swap. The standard move is **Parameterize Method / Introduce Instance Delegator**, or wrapping the static call in an overridable instance method so a seam exists. ## Link seams Substitution happens when the program is assembled rather than when it runs. Examples: - Compile the test build against a stub `.o`/`.lib` that provides the same symbols as the real one (classic C/C++). - Put a fake jar earlier on the classpath, or supply a different module/package that resolves the same import (JVM, .NET, Node module resolution, Python `sys.path`). - Swap a dynamically linked shared library via `LD_PRELOAD` or an equivalent loader trick. Enabling point: the build script, classpath/module path, or linker configuration — not the source. Trade-offs: link seams are the only option when you must neutralize free functions, system calls, or a third-party binary you cannot modify. But they are **invisible in the source**: a reader of the code has no clue behaviour is substituted, and a mis-ordered classpath can silently change production behaviour. Debugging "why is the fake loaded in prod?" is genuinely painful. Use them as a bridge, not a destination. ## Preprocessing / build seams Substitution happens before or during compilation: - C/C++ preprocessor macros redefining a function name to a test double. - Conditional compilation (`#if TEST`, build-flavour source sets). - Source or bytecode generation/weaving (aspects, instrumentation agents, compile-time DI). Enabling point: the build flag or macro definition. Trade-offs: powerful and available where nothing else is, but the code you read is not the code that runs, `#if`-riddled sources are hard to reason about, and test-only branches can drift out of sync with the shipped configuration. Feathers ranks these last for exactly that reason. ## Choosing a seam Rough decision order: 1. **Does an object seam already exist?** Use it — zero-risk, no production change. 2. **Can you create one with a mechanical, signature-preserving refactoring?** Extract Interface, Extract and Override Factory Method, Parameterize Constructor, Subclass and Override Method. This is the normal answer and improves the design (dependency inversion) as a side effect. 3. **Is the obstacle a static call, free function, or third-party binary?** Consider a link or preprocessing seam, or wrap the call so an object seam becomes possible. 4. **Is the obstacle system-level (clock, filesystem, environment, randomness)?** Introduce an explicit abstraction and inject it — these are the highest-value seams because they also remove flakiness. ## Related concepts - **Sensing vs separation.** Separation = keeping bad things from happening (no real charge). Sensing = observing effects you cannot otherwise see (recording that a charge *would* have happened). One seam often serves both, with a fake that both no-ops and records. - **Seams vs mocking frameworks.** A mocking library that rewrites bytecode or patches modules can simulate a seam without changing the design. It is convenient, but it hides coupling and can make tests brittle to internal structure. Feathers' preference is to create a real seam so the design improves permanently. - **Pinch points.** A narrow interface through which many behaviours flow is a natural place to put characterization tests; it is a seam that is also an economical test point.
- Your legacy method calls a static singleton like Clock.now() deep inside. What seam options do you have, ranked?Best: introduce an injected clock abstraction and pass a fixed clock in tests (creates an object seam and removes time flakiness permanently). Cheaper interim: wrap the static call in a protected instance method and override it in a test subclass. Language-specific fallbacks: a link seam (substitute the library providing the symbol) or a preprocessing/instrumentation seam; and in dynamic languages, monkey-patching — all of which hide the coupling and should be temporary.
- Why does Feathers prefer object seams to link seams even though link seams require no production code change?Because link seams are invisible at the call site, depend on fragile build/classpath configuration that can misfire in production, and leave the design untouched — the code stays hard to test forever. Object seams cost a small, mechanical production change but make the dependency explicit and permanently improve testability.
- Is using a mocking framework that stubs static methods the same as creating a seam?It achieves substitution without a design change, so it is a pragmatic shortcut, but it is not a seam in Feathers' sense: there is no enabling point in the program's own structure, tests become coupled to internal implementation details, and the production design remains untestable by ordinary means.
A seam in tailoring is where two pieces of fabric are joined — you can take the garment apart there without cutting the cloth. In code it is the join you can open to slip in a substitute, and the enabling point is the stitch you actually pull.
saying these in an interview costs you the question
- Describing a seam as 'any place tests can inject a mock' without mentioning the enabling point
- Claiming a direct static or free-function call is an object seam
- Treating link/preprocessing seams as equally good long-term choices
- Saying seams are only about avoiding databases — sensing side effects matters just as much
- Introducing an interface with a single implementation everywhere 'for seams' regardless of need, creating ceremony without value