Bridge

June 17, 2026 in patterns 5 minutes

A intermediate-level guide to Bridge: before-and-after java code and diagrams for a CS student.

The Class Explosion Problem

Imagine you are building a graphics engine. Initially, you only have two shapes: Circles and Squares. You implement them using inheritance to handle different colors, creating RedCircle and BlueCircle.

The problem arises when your requirements grow. If you add a third color, Green, you must create GreenCircle and GreenSquare. If you then decide to support a new shape, Triangle, the explosion begins: you need RedTriangle, BlueTriangle, and GreenTriangle.

As you can see in this diagram, adding just one new dimension (like a new color or a new shape type) forces you to multiply your existing classes. This is a “class explosion” caused by multi-dimensional inheritance.

  classDiagram
    class Shape {
        <<abstract>>
        +draw() void
    }
    class RedCircle {
        +draw() void
    }
    class BlueCircle {
        +draw() void
    }
    class RedSquare {
        +draw() void
    }
    class BlueSquare {
        +draw() void
    }
    Shape <|-- RedCircle
    Shape <|-- BlueCircle
    Shape <|-- RedSquare
    Shape <|-- BlueSquare
// Problem: Class explosion due to multi-dimensional inheritance.
// To add a new color OR a new shape, we must create multiple new subclasses.

abstract class Shape {
    abstract void draw();
}

class RedCircle extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a red circle");
    }
}

class BlueCircle extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a blue circle");
    }
}

class RedSquare extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a red square");
    }
}

class BlueSquare extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a blue square");
    }
}

Separating Abstraction from Implementation

The Bridge pattern solves this by replacing inheritance with composition. Instead of trying to define a class that is both a “Circle” and “Red,” we split the problem into two separate hierarchies:

  1. Abstraction: The high-level control logic (the Shape itself).
  2. Implementation: The platform-specific or detail-oriented work (the Color).

Instead of inheriting color properties, a Shape holds a private reference to a Color object. When the Shape needs to perform an action involving color, it delegates that task to its color reference. This is known as Delegation.

By using this “bridge” between the two hierarchies, adding a new shape only requires one new class, and adding a new color only requires one new class. The growth becomes linear rather than exponential.

  classDiagram
    class Shape {
        <<abstract>>
        #color: Color
        +draw() void
    }
    class Circle {
        +draw() void
    }
    class Square {
        +draw() void
    }
    class Color {
        <<interface>>
        +apply() void
    }
    class RedColor {
        +apply() void
    }
    class BlueColor {
        +apply() void
    }

    Shape <|-- Circle
    Shape <|-- Square
    Shape o-- Color : color
    Color <|.. RedColor
    Color <|.. BlueColor

To understand how these objects interact at runtime, look at how a Circle uses its delegated color object to fulfill a request.

  sequenceDiagram
    participant S as Circle
    participant C as RedColor
    S->>C: apply()
    Note over C: Prints "red"
    C-->>S: done
// Solution: Bridge pattern.
// We separate the Abstraction (Shape) from its Implementation (Color).

interface Color {
    void apply();
}

class RedColor implements Color {
    @Override
    public void apply() {
        System.out.println("red");
    }
}

class BlueColor implements Color {
    @Override
    public void apply() {
        System.out.println("blue");
    }
}

abstract class Shape {
    protected final Color color;

    protected Shape(Color color) {
        this.color = color;
    }

    abstract void draw();
}

class Circle extends Shape {
    Circle(Color color) { super(color); }

    @Override
    void draw() {
        System.out.print("Drawing a ");
        color.apply();
        System.out.println(" circle");
    }
}

class Square extends Shape {
    Square(Color color) { super(color); }

    @Override
    void draw() {
        System.out.print("Drawing a ");
        color.apply();
        System.out.println(" square");
    }
}

Technical Breakdown: The Roles in Bridge

To implement this pattern successfully, you need to understand the four distinct roles:

  • Abstraction: Defines the interface for the client (e.g., Shape). It maintains a reference to an object of type Implementor.
  • Refined Abstraction: Extends the abstraction and provides specialized versions of the high-level logic (e.g., Circle and Square).
  • Implementor: Defines the interface for implementation classes (e.g., Color). This interface is separate from the abstraction so they can vary independently.
  • Concrete Implementors: The actual platform-specific or detail-oriented implementations (e.g., RedColor and BlueColor).

Bridge vs. Strategy

While both use composition to delegate work, they serve different purposes. Bridge is a structural pattern used upfront during design to prevent a permanent hierarchy explosion. It separates two dimensions of a class (what it does vs. how it does the details). Strategy is a behavioral pattern typically used at runtime to swap out algorithms for a specific task.

When to Use It, and When Not To

The Bridge pattern is an investment in future flexibility.

Use it when:

  • You want to avoid a permanent binding between an abstraction and its implementation.
  • You have two dimensions of variation (like Shape and Color, or RemoteControl and Device) that are likely to grow independently.
  • You want to share an implementation among multiple objects.

Avoid it when:

  • Your class hierarchy is simple and unlikely to grow in multiple directions. Applying Bridge to a single-dimension hierarchy adds unnecessary complexity through extra interfaces and delegation.
  • The performance overhead of the extra method call (the delegation hop) is unacceptable for your specific high-frequency loop.

Takeaways

  • Bridge uses composition to separate an object’s interface from its implementation, preventing class explosion.
  • It turns exponential growth in subclasses into linear growth across two independent hierarchies.
  • Use it when you anticipate multiple axes of change; avoid it if the complexity of the pattern outweighs the benefits of the abstraction.

Usage

Using the Bridge to compose shape and color independently

Shape circle = new Circle(new RedColor());
circle.draw(); // Drawing a red circle