skip to content

What is the 'class explosion' problem, and how does Bridge avoid it in a Java design?

level: juniorimportance: should knowfreq 35%

answer

  1. Two axes encoded by inheritance = M×N subclasses
  2. Bridge separates axes → M+N classes
  3. Combinations made at runtime by pairing objects
  4. Additive growth, no per-renderer duplication
  5. Skip Bridge if only one axis varies

basics

~20 s

If something changes along two directions and you make a subclass for every combination, the number of classes multiplies (M×N). Bridge puts each direction in its own group and links them with a reference, so you only need M+N classes.

solid answer

~50 s

'Class explosion' happens when a type varies along two (or more) independent dimensions and you try to capture every combination with inheritance. With M values on one axis and N on the other, you end up needing M×N subclasses — e.g. shapes × renderers gives VectorCircle, RasterCircle, VectorSquare, and so on. Adding one new value on either axis forces a whole new row or column of classes, and the duplication grows multiplicatively. Bridge fixes this by separating the two axes into independent hierarchies and connecting them by composition: the abstraction holds a reference to an implementation. Now you write M abstraction classes and N implementation classes — M+N total — and compose any pair at runtime. One new renderer is one new class that every existing shape can immediately use. The cost is a layer of indirection, but it scales additively instead of multiplicatively.

code

java · 26 lines
java
// WITHOUT Bridge: M x N subclasses (explosion)
// class VectorCircle ... class RasterCircle ... class VectorSquare ... etc.

// WITH Bridge: M + N classes, combined at runtime
interface Renderer { void circle(double r); void square(double side); }
class VectorRenderer implements Renderer {
    public void circle(double r) { /* vector */ }
    public void square(double s) { /* vector */ }
}
class RasterRenderer implements Renderer {
    public void circle(double r) { /* raster */ }
    public void square(double s) { /* raster */ }
}
abstract class Shape {
    protected final Renderer r;        // bridge
    Shape(Renderer r) { this.r = r; }
    abstract void draw();
}
class Circle extends Shape {
    private final double radius;
    Circle(double radius, Renderer r) { super(r); this.radius = radius; }
    void draw() { r.circle(radius); }
}
// Any pairing at runtime — no VectorCircle/RasterCircle classes needed:
// new Circle(2, new VectorRenderer());
// new Circle(2, new RasterRenderer());

go deeper

for a junior

Can explain in plain words that combining two axes via subclassing multiplies the class count, and Bridge keeps it additive by holding a reference.

for a middle

Can give the M×N vs M+N numbers, show the runtime-pairing code, and state when Bridge isn't worth it (single axis).

for a senior

Frames it as the cost of multiplicative duplication versus a single layer of indirection, and judges when the second axis is real enough to justify the pattern.

for a principal

Generalizes to keeping variation dimensions orthogonal across a codebase, and weighs the indirection/discoverability cost against future-proofing both axes.

## Setting the scene: two axes of variation Many types naturally change along more than one **axis** (an independent dimension of variation). A `Shape` varies by *kind* (circle, square, triangle) and also by *how it's rendered* (vector graphics, raster pixels). A message `Notification` varies by *type* (alert, reminder) and by *channel* (email, SMS, push). ## What goes wrong with pure inheritance Inheritance lets one subclass fix one choice. If you try to encode *both* axes with subclasses, you must make a subclass for **every combination**: ``` VectorCircle, RasterCircle, VectorSquare, RasterSquare, VectorTriangle, RasterTriangle ``` With **M** kinds and **N** renderers that's **M×N** classes. This is the **class explosion** (or 'combinatorial explosion'). Two painful consequences: 1. **Multiplicative growth.** Add a third renderer and you must add M new classes (one per shape). Add a fourth shape and you add N new classes. 2. **Duplication.** The rendering logic for 'vector' is copied across every `Vector*` class; fixing a bug means editing many files. ## How Bridge defeats it Bridge separates the axes into two **independent hierarchies** and wires them with a **reference** (composition) instead of inheritance: - Shape hierarchy: `Circle`, `Square`, `Triangle` (M classes). - Renderer hierarchy: `VectorApi`, `RasterApi` (N classes). - Each shape **holds** a renderer and delegates the drawing primitives to it. Now the total is **M + N** classes. The combinations are produced at **runtime** by pairing a shape object with a renderer object (`new Circle(new RasterApi())`), not at compile time by subclassing. Add a renderer → one new class, and *every* existing shape can use it instantly. Growth is **additive**, and the rendering logic lives in exactly one place per renderer. ## Quick numeric intuition - 3 shapes × 3 renderers: inheritance = 9 classes; Bridge = 6. - 10 shapes × 10 renderers: inheritance = 100; Bridge = 20. - The gap widens fast as either axis grows — that's the whole point. ## Terms defined - **Axis of variation / dimension**: an independent way a type can differ. - **Combinatorial / class explosion**: the M×N growth in subclasses when you encode multiple axes via inheritance. - **Composition**: holding a reference to a collaborator object and delegating to it, instead of subclassing. - **Delegation**: forwarding a method call to the held collaborator. ## When it's NOT worth it If there's only **one** axis of variation (or the second axis will never grow), Bridge just adds indirection for no benefit — plain subclassing or a single interface is simpler. Bridge pays off precisely when both axes are real and expected to grow.

  • When is Bridge overkill?
    When only one dimension actually varies, or the second dimension is fixed and won't grow. Then the extra indirection buys nothing and plain subclassing or a single interface is simpler.
  • Does Bridge make zero subclasses?
    No — you still have M abstraction classes and N implementation classes. It converts the multiplicative M×N count into the additive M+N count.

saying these in an interview costs you the question

  • Thinking the explosion happens with one axis of variation (it needs two or more)
  • Believing Bridge eliminates classes entirely rather than turning M×N into M+N
  • Applying Bridge when the second axis will never grow (needless indirection)
  • Confusing the runtime pairing with compile-time subclass combinations

context