skip to content

What problem does the Bridge design pattern solve, and how does its structure solve it?

level: juniorimportance: must knowfreq 55%

answer

  1. M×N classes → M+N classes
  2. Two hierarchies, one reference
  3. "What" vs "how"; Handle/Body
  4. Composition replaces the second inheritance axis
  5. Implementor = primitive ops, not a mirror

basics

~20 s

Bridge splits a design into two separate hierarchies — an abstraction (what the thing is) and an implementation (how it does its work) — connected by a reference instead of inheritance, so each side can grow independently.

solid answer

~50 s

Bridge attacks the combinatorial class explosion you get when one type varies along two independent dimensions and you express both with inheritance. If you have 3 shape kinds and 3 rendering backends, subclassing gives you 9 leaf classes (VectorCircle, RasterCircle, ...), and adding a fourth backend costs 3 more. Bridge keeps the two dimensions in two hierarchies: an Abstraction (Shape, with refinements Circle/Square) holding a reference to an Implementor interface (Renderer, with implementations Vector/Raster). Shape delegates primitive operations to the Renderer it was given. Now 3 + 3 = 6 classes, and adding a backend costs exactly one class. The composition link — the 'bridge' — is set at construction or configuration time, which also means the pairing can be chosen at runtime rather than baked in at compile time. Cost: one extra level of indirection and a designed-up-front interface between the halves.

code

pseudocode · 25 lines
pseudocode
// --- Implementor: the "how" (primitive operations) ---
interface Renderer {
  drawCircle(x, y, radius)
  drawLine(x1, y1, x2, y2)
}
class VectorRenderer implements Renderer { ... }
class RasterRenderer implements Renderer { ... }

// --- Abstraction: the "what" ---
abstract class Shape {
  protected renderer: Renderer      // <-- the bridge
  constructor(r: Renderer) { this.renderer = r }
  abstract draw()
}
class Circle extends Shape {
  draw() { renderer.drawCircle(x, y, radius) }   // delegates; never touches pixels
}
class Square extends Shape {
  draw() { renderer.drawLine(...) x4 }
}

// Client picks the pairing at runtime:
shape = new Circle(new RasterRenderer())
// 2 shapes + 2 renderers = 4 classes, not 4 combination classes;
// adding SvgRenderer costs exactly one class.

go deeper

for a junior

State the intent — separate what from how so each varies independently — and give the M×N → M+N counting argument with a concrete pair of axes.

for a middle

Name the four participants (Abstraction, RefinedAbstraction, Implementor, ConcreteImplementor), show who holds the reference, and note that the pairing can be chosen at runtime.

for a senior

Discuss designing the primitive Implementor interface (granularity, why it must not mirror the Abstraction), who instantiates the implementor, and when Bridge is over-engineering.

for a principal

Frame it as a versioning and deployment boundary — the bridge interface is a contract two independently evolving halves depend on (pimpl/ABI stability, plugin SPIs, ports in hexagonal architecture) — and reason about the cost of ever changing it.

## The problem: two dimensions of variation Software types often vary along more than one **independent axis**. A drawing object varies by *shape* (circle, square, polygon) and by *how it is rendered* (vector output, raster output, SVG, a GPU API). A notification varies by *kind* (alert, reminder, digest) and by *delivery channel* (email, SMS, push). If you model both axes with **inheritance** (subclassing), you must create one concrete class per combination: ``` Shape ├─ VectorCircle RasterCircle SvgCircle ├─ VectorSquare RasterSquare SvgSquare └─ VectorPolygon RasterPolygon SvgPolygon ``` That is `M × N` classes. This is called the **class explosion** (or combinatorial explosion). It has three practical costs: 1. **Growth is multiplicative.** Adding one new renderer adds M new classes; adding one new shape adds N. 2. **Duplication.** The shape-specific geometry logic is copied across every renderer variant, and the renderer-specific drawing logic is copied across every shape variant. Fixing a bug means fixing it in many places. 3. **Compile-time binding.** The pairing (`VectorCircle`) is fixed when you write the class; you cannot pick a renderer at runtime without a factory that knows every combination. ## The Bridge solution Bridge (from the *Design Patterns* / "Gang of Four" catalogue, in its **structural** category) says: **stop expressing the second axis with inheritance; express it with composition.** The canonical participants: - **Abstraction** — the high-level type clients talk to (`Shape`). It holds a reference to an Implementor and defines operations in terms of it. - **RefinedAbstraction** — subclasses that extend or specialise the abstraction (`Circle`, `Square`). This hierarchy captures axis 1. - **Implementor** — an interface of *primitive* operations (`Renderer` with `drawCircle`, `drawLine`, `fill`). Note: it is **not** a mirror of the Abstraction's interface; it is a lower-level vocabulary the Abstraction is written on top of. - **ConcreteImplementor** — the actual implementations (`VectorRenderer`, `RasterRenderer`). This hierarchy captures axis 2. The reference from Abstraction to Implementor is *the bridge*. Client code creates a concrete implementor and passes it into the abstraction (constructor injection is typical). Result: `M + N` classes instead of `M × N`. Adding a renderer = **one** new class, zero changes to any shape. Adding a shape = one new class, zero changes to any renderer. ## Why the name "abstraction" is confusing In Bridge, "Abstraction" does **not** mean "abstract class" and "Implementation" does **not** mean "the concrete subclass of that abstract class." Both sides are their own abstract/concrete hierarchies. Read them as **"the what" (domain-facing, higher level) vs "the how" (mechanism-facing, lower level)**. GoF's own alternate name for the pattern, **Handle/Body**, captures this better: a thin handle object clients hold, and a swappable body object doing the work. C++'s **pimpl** (pointer-to-implementation) idiom is exactly this, used to hide implementation details from a header file and keep the binary interface stable. ## Mechanics and edge cases - **Who creates the Implementor?** Usually the client, or a factory/DI container. Sometimes the Abstraction picks a default lazily ("if no renderer was supplied, use the platform default"). Letting the Abstraction hard-code a concrete implementor re-couples the halves and undoes the pattern. - **Can the implementor be swapped at runtime?** Yes, if you expose a setter or rebuild the abstraction. This is a real benefit over inheritance: the same `Circle` instance can be re-pointed at a different renderer. - **Interface granularity.** If the Implementor interface is too *narrow*, abstractions must synthesise missing behaviour awkwardly; too *wide*, and every new concrete implementor must implement operations it doesn't care about. Choosing the primitive set is the hard design work in Bridge. - **Degenerate case.** With exactly one implementor, Bridge is over-engineering — you have paid indirection for no variation. It earns its keep when the second axis genuinely has (or will have) multiple values, or when the two sides must be *compiled/deployed/versioned* separately. ## Trade-offs summary **Gains:** linear rather than multiplicative growth; independent evolution and independent deployment of the two sides; runtime binding; implementation details hidden from clients; better testability (inject a fake implementor). **Costs:** one more indirection per call; two hierarchies to navigate, so the code is less obvious to a newcomer; you must design the primitive interface *up front* and it is expensive to change later because both sides depend on it.

  • What happens to a Bridge design when a third independent dimension appears (say, shape × renderer × coordinate system)?
    Bridge does not automatically generalise to three axes: chaining a second bridge (the renderer itself holding a coordinate-system reference) works if the axes are genuinely layered, but if all three are peers you often move to composition-by-parts or a strategy-per-axis object, or accept a small explosion on the least-volatile axis. Some designs push the third axis into an Abstract Factory that assembles a consistent triple.
  • Does the Implementor interface have to mirror the Abstraction's interface?
    No — and it usually shouldn't. If it mirrors it one-to-one you have a delegating wrapper, not a real bridge. The Implementor should expose lower-level primitives that the Abstraction composes into higher-level operations.

A TV remote and a TV. Remotes vary (basic, advanced, voice); devices vary (TV, radio, projector). You do not build a BasicTvRemote, AdvancedTvRemote, BasicRadioRemote... — the remote holds a reference to a device and sends generic commands like power/volume. Any remote works any device; new devices need no new remotes.

saying these in an interview costs you the question

  • Saying "Abstraction means the abstract class and Implementor means its subclass" — they are two separate hierarchies
  • Claiming Bridge is about hiding implementation details only; hiding is a side effect, the intent is independent variation of two axes
  • Making the Implementor interface an exact mirror of the Abstraction's interface (that is a wrapper, not a bridge)
  • Having the Abstraction construct a hard-coded concrete Implementor, which re-couples the halves
  • Applying Bridge when there is exactly one implementation and no realistic second one — pure indirection tax

context