What are the main costs and failure modes of the Bridge pattern, and how do you tell when you have applied it badly?
answer
- Seam is the expensive part, not the indirection
- Mirror interface = wrapper, not bridge
- `is SvgRenderer` branches = wrong primitive level
- Fat interface → unsupported-operation stubs (ISP/LSP)
- Add-one test: new implementor = 1 class, 0 edits
basics
~20 sBridge adds indirection and a second hierarchy to navigate, and forces you to design the interface between them early. It goes wrong when the interface mirrors the abstraction, leaks implementation details, or exists with only one implementation.
solid answer
~50 sThe costs: an extra indirection on every call (usually negligible, occasionally relevant in hot inner loops or where it defeats inlining); two hierarchies plus an assembly step, so reading the code requires jumping between files; and — the real one — you must design the primitive interface **up front**, and changing it later breaks every implementor, including any outside your repository. Failure modes to look for: (1) the implementor interface mirrors the abstraction one-to-one, making it a pointless wrapper; (2) the abstraction branches on implementor identity or capability flags (`if (renderer is SvgRenderer)`), meaning the seam is at the wrong level; (3) the interface keeps growing because each new implementor adds methods only it needs, so others must throw "not supported"; (4) exactly one implementor exists and none is planned; (5) leaked implementor types in the abstraction's public signatures, so clients now depend on both halves. The health test is cheap: adding a new implementor should be one new class with zero edits elsewhere.
go deeper
Mention the two obvious costs — extra indirection and more classes/files to follow — and that it isn't worth it with a single implementation.
Add concrete smells: a mirror interface, type checks on the implementor, and the add-one health test.
Lead with the up-front seam-design cost, discuss interface bloat against ISP/LSP, leaked implementor types, sparse matrices, and be willing to argue for removing a bridge that never paid off.
Treat the interface as a released contract: versioning strategy for out-of-tree implementors, default methods and adapters for evolution, ownership rules for who may change it, and an explicit bar for when the seam is justified.
## What Bridge actually costs **1. Indirection.** Every operation goes abstraction → implementor. In most systems this is invisible. It matters when the call is in a tight numeric loop, when the virtual dispatch defeats compiler inlining, or in constrained runtimes. Measure before optimising; the usual outcome is that the design cost dominates the runtime cost. **2. Cognitive cost.** A reader tracing `notification.notify()` must find the concrete implementor, which was chosen somewhere else entirely (a factory, a DI container, a config file). Indirection you cannot follow in an IDE — for instance, when the binding is data-driven — is materially harder to maintain. Bridge deliberately moves the *pairing decision* away from the call site; make sure it lands in one obvious place. **3. The up-front interface decision.** This is the expensive one. The implementor interface is a **contract two independently evolving hierarchies depend on**. Once you have several implementors — worse, implementors written by other teams or other companies (plugins, SPIs, drivers) — adding or changing a method is a breaking change with a migration cost proportional to the number of implementors. Bridge trades cheap future extension *along* the axes for expensive change *to the seam itself*. **4. Object assembly.** Clients now construct two things and wire them. Left unmanaged, `new Circle(new RasterRenderer())` spreads across the codebase and a global change means touching every call site. Factories, builders or a DI container are effectively mandatory companions. ## Failure modes, with diagnostics **Mirror interface.** `interface Channel { notify(Notification n) }` — the implementor's methods echo the abstraction's. Every implementor now understands the abstraction's model, so the two halves are coupled again and adding an abstraction member can force implementor changes. *Diagnostic:* the abstraction's methods are one-line forwards. *Fix:* re-express the implementor as primitives the abstraction composes (several calls, conditionals, ordering logic on the abstraction side). **Type-checking the implementor.** `if (renderer is SvgRenderer) { ... }` or a proliferation of `supportsX()` capability queries used everywhere. The abstraction is compensating for a badly chosen primitive set. *Fix:* raise or lower the primitive level so behaviour differences live inside implementors; if capabilities genuinely differ, model them explicitly and honestly (e.g. an optional capability sub-interface plus one clear negotiation point) rather than sprinkling branches. **Interface bloat / "fat implementor".** Each new implementor pushes one more method onto the shared interface, and other implementors stub it out with "unsupported operation" errors. This violates the Interface Segregation Principle (clients shouldn't depend on methods they don't use) and breaks Liskov substitutability (an implementor that throws where the contract promises work is not a valid substitute). *Fix:* split into a small core interface plus optional capability interfaces, or provide safe defaults. **Single implementor.** No second axis exists, so no explosion was prevented; you bought indirection for testability alone. That is a fine reason — just call it dependency injection, and don't grow the ceremony. **Leaked implementor types.** If `Shape.getRenderer(): RasterRenderer` or the abstraction's public methods take implementor-specific types, clients now compile against both hierarchies and you can no longer swap or version them independently. **Sparse or dependent matrix.** If half the abstraction × implementor cells are nonsense or need special-casing, the axes were not orthogonal. Symptom: conditionals inside implementors keyed on which abstraction called them. *Fix:* reconsider the split, or accept some combination classes for the genuinely special cases. ## Health checks you can run 1. **Add-one test.** Adding an implementor = one new class, no edits elsewhere. Adding a refined abstraction = one new class, no implementor edits. If either fails, the seam is wrong. 2. **Grep for concrete implementor names** in the abstraction hierarchy. There should be none (aside from a single default-selection point). 3. **Count forwards.** If most abstraction methods are single-line delegations, you have a wrapper, not a bridge. 4. **Ask who may change the interface.** If the answer is "anyone, any time," you don't yet appreciate the seam's cost; write it down as a versioned contract or accept churn. ## When to remove a Bridge Refactoring *away* is legitimate: if one implementor has been the only one for years and no plugin story exists, inlining it and deleting the interface reduces cost with no loss. Patterns are reversible; treating an existing Bridge as untouchable is its own failure mode.
- Your bridge interface has grown to 18 methods and three of five implementors throw "unsupported" for six of them. What do you do?Split it. Keep a small mandatory core that every implementor can honour, and move optional behaviour into separate capability interfaces that clients query once and branch on deliberately, or provide safe no-op/derived defaults. Throwing from a declared method breaks substitutability, so callers cannot trust the contract — that is the concrete harm, not just aesthetics.
- Does Bridge's extra indirection ever matter for performance?Rarely, but yes in hot loops where virtual dispatch blocks inlining, in tight numeric or graphics kernels, or in constrained runtimes. The mitigations are the usual ones: batch at a coarser granularity so one bridged call does more work, or specialise the hot path. Measure first — the maintenance cost of the seam is almost always the bigger number.
- How do you evolve a bridge interface that external plugin authors implement?Treat it as a released API: add rather than change, ship new methods with defaults where the language allows so existing implementors keep compiling, version the interface (V2 alongside V1) when a breaking change is unavoidable, and provide an adapter from old to new. Announce deprecation windows. This is why the up-front interface design cost is real.
saying these in an interview costs you the question
- Treating Bridge as free because "it's just an interface" — the seam is a contract with real evolution cost
- Adding methods to the shared implementor interface for one implementor and stubbing the rest with unsupported-operation errors
- Exposing concrete implementor types in the abstraction's public API
- Assuming a Bridge, once introduced, can never be refactored away
- Blaming performance problems on Bridge indirection without measuring