00:00

Bridge Design Pattern in Java

Last updated:

Think of a TV remote control. The remote decides what you can do: power on, volume up, mute. The TV decides how each action is carried out. You can buy a new remote without changing the TV, or a new TV that works with the same remote. The Bridge Design Pattern gives your Java classes the same freedom.

This tutorial explains what the Bridge pattern is, the problem it solves, its structure, and four complete Java examples. It also shows where the JDK uses it, how it differs from Adapter and Strategy, and when you should and should not use it.

1. What Is the Bridge Design Pattern?

The Bridge Design Pattern is a structural design pattern that separates an abstraction from its implementation so that the two can vary independently.

In simple words, it splits one large class, or a set of closely related classes, into two separate hierarchies:

  • Abstraction: the high-level part the client talks to (the remote control).
  • Implementation: the low-level part that does the actual work (the TV or radio).

Instead of linking them through inheritance, the abstraction holds a reference to an implementation object and delegates work to it. That reference is the bridge. This is the principle of composition over inheritance applied to two dimensions at once.

GoF definition: "Decouple an abstraction from its implementation so that the two can vary independently." The Bridge pattern is one of the 23 patterns in Design Patterns: Elements of Reusable Object-Oriented Software.

2. The Problem: Class Explosion

Without the Bridge pattern, code that varies in two directions quickly becomes hard to maintain. Suppose you have:

  • Different shapes: Circle, Square
  • Different colors: Red, Blue

If you use inheritance for every combination, you end up with RedCircle, BlueCircle, RedSquare and BlueSquare. Add a Triangle and you need two more classes. Add Green and you need three more. The number of classes is the product of the two dimensions.

With the Bridge pattern you write one class per shape and one class per color, so the number of classes is the sum:

ShapesColorsInheritance (shapes × colors)Bridge (shapes + colors)
224 classes4 classes
339 classes6 classes
5420 classes9 classes
10550 classes15 classes

The Bridge pattern solves this problem by:

  • Keeping shape logic in its own hierarchy
  • Keeping color logic in its own hierarchy
  • Connecting the two with a single reference, the bridge

3. Key Components and Structure

ComponentRoleIn the shapes example
AbstractionDefines the high-level interface for the client and holds a reference to an ImplementorShape
Refined AbstractionExtends the Abstraction with specific variants or extra behaviorCircle, Square
ImplementorInterface for the low-level operations the Abstraction relies onColor
Concrete ImplementorProvides the actual platform- or detail-specific implementationRed, Blue
ClientPicks a concrete implementor, passes it to an abstraction and uses the abstractionBridgePatternDemo
Bridge pattern structure
        ABSTRACTION SIDE                         IMPLEMENTATION SIDE

  +-----------------------+    the bridge     +-----------------------+
  |  Shape  (abstract)    |------------------>|  <<interface>> Color  |
  |-----------------------|      has-a        |-----------------------|
  |  # color : Color      |                   |  + fill() : String    |
  |  + draw()             |                   +-----------------------+
  +-----------------------+                               ^
              ^                                           | implements
              | extends                           +-------+-------+
       +------+------+                            |               |
       |             |                           Red             Blue
    Circle         Square                  (Concrete Implementors)
  (Refined Abstractions)

Notice that Circle never extends Red. A circle has a color; it is not a kind of color. Each hierarchy can grow without touching the other.

4. Bridge Pattern Examples in Java

The four examples below apply the same structure to different problems. Select a tab to switch between them; each one is complete and runnable on its own.

Example 1: Shapes and Colors

Let us fix the class explosion from section 2, step by step.

Step 1: Create the Implementor Interface

Color.java
public interface Color {

    String fill();
}

Step 2: Create the Concrete Implementors

Red.java
public class Red implements Color {

    @Override
    public String fill() {
        return "red";
    }
}
Blue.java
public class Blue implements Color {

    @Override
    public String fill() {
        return "blue";
    }
}

Step 3: Create the Abstraction

The abstraction holds the bridge: a Color reference passed in through the constructor.

Shape.java
public abstract class Shape {

    // The bridge: a reference to the implementation hierarchy
    protected final Color color;

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

    public abstract void draw();
}

Step 4: Create the Refined Abstractions

Circle.java
public class Circle extends Shape {

    private final int radius;

    public Circle(int radius, Color color) {
        super(color);
        this.radius = radius;
    }

    @Override
    public void draw() {
        System.out.println("Drawing a circle of radius " + radius
                + " filled with " + color.fill());
    }
}
Square.java
public class Square extends Shape {

    private final int side;

    public Square(int side, Color color) {
        super(color);
        this.side = side;
    }

    @Override
    public void draw() {
        System.out.println("Drawing a square of side " + side
                + " filled with " + color.fill());
    }
}

Step 5: Test the Bridge Pattern

BridgePatternDemo.java
public class BridgePatternDemo {

    public static void main(String[] args) {
        Shape redCircle = new Circle(5, new Red());
        Shape blueCircle = new Circle(8, new Blue());
        Shape blueSquare = new Square(4, new Blue());

        redCircle.draw();
        blueCircle.draw();
        blueSquare.draw();
    }
}

Output

Console
Drawing a circle of radius 5 filled with red
Drawing a circle of radius 8 filled with blue
Drawing a square of side 4 filled with blue
Try it: add a Green color. You write one new class, and every existing shape can use it immediately. With inheritance you would need GreenCircle and GreenSquare.

5. Choosing the Implementation at Runtime

Because the bridge is a plain object reference, the client can pick the implementor from configuration, user input or environment, without any if chains inside the abstraction:

SenderSelection.java
public class SenderSelection {

    static MessageSender senderFor(String channel) {
        switch (channel) {
            case "sms":
                return new SmsSender();
            case "email":
                return new EmailSender();
            default:
                throw new IllegalArgumentException("Unknown channel: " + channel);
        }
    }

    public static void main(String[] args) {
        String channel = System.getProperty("notify.channel", "email");

        Notification reminder = new ReminderNotification(senderFor(channel));
        reminder.deliver("user@example.com", "Your trial ends tomorrow");
    }
}

Run it with -Dnotify.channel=sms and the same ReminderNotification sends an SMS instead of an email. Keeping the bridge field final and choosing the implementor at construction time, as shown here, is usually safer than exposing a setter.

6. Bridge Pattern in the JDK: JDBC

JDBC is the best-known Bridge in Java. Your code works against the java.sql interfaces (the abstraction), and each database vendor ships a driver that implements them (the implementation):

UserRepository.java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class UserRepository {

    private final String url;
    private final String user;
    private final String password;

    public UserRepository(String url, String user, String password) {
        this.url = url;
        this.user = user;
        this.password = password;
    }

    public String findNameById(long id) throws SQLException {
        String sql = "SELECT name FROM users WHERE id = ?";

        try (Connection connection = DriverManager.getConnection(url, user, password);
             PreparedStatement statement = connection.prepareStatement(sql)) {

            statement.setLong(1, id);

            try (ResultSet resultSet = statement.executeQuery()) {
                return resultSet.next() ? resultSet.getString("name") : null;
            }
        }
    }
}

Change the URL from jdbc:mysql://... to jdbc:postgresql://... and add the PostgreSQL driver to the classpath. UserRepository does not change. Other examples of the same idea include SLF4J with Logback or Log4j 2 bindings, and AWT components with their native peers.

7. Bridge vs Adapter vs Strategy

These patterns all use composition and can look alike in code. The difference is intent:

AspectBridgeAdapterStrategy
CategoryStructuralStructuralBehavioral
IntentLet abstraction and implementation vary independentlyMake an incompatible interface work with existing codeSwap one algorithm inside a class
When appliedDesigned up frontApplied after the fact to existing classesDesigned up front
HierarchiesTwo, both may have subclassesAdapter wraps one adapteeOne context, many strategies
Typical exampleRemote and device, JDBCArrays.asList, InputStreamReaderComparator passed to a sort

A quick rule: if the wrapper side also has its own subclasses that vary independently, it is a Bridge. If only the behavior is swapped, it is a Strategy. If you are fixing a mismatch between existing interfaces, it is an Adapter.

8. Advantages and Disadvantages

Advantages

  • Prevents class explosion: m + n classes instead of m × n.
  • Independent extension: add abstractions or implementations without touching the other side (Open/Closed Principle).
  • Single Responsibility: high-level logic and platform details live in separate classes.
  • Hides implementation details from the client, which only sees the abstraction.
  • Easier testing: pass a fake or mock implementor to test the abstraction in isolation.
  • Runtime flexibility: choose the implementation from configuration.

Disadvantages

  • More types and indirection, which can feel heavy for small problems.
  • Harder to design up front: you must identify the two independent dimensions correctly.
  • Overkill when there is only one implementation or only one dimension of change.

9. When to Use the Bridge Pattern

Use it when:

  • A class varies in two or more independent dimensions, such as type and platform, or message and channel.
  • You want to avoid a permanent binding between an abstraction and its implementation.
  • Changes to the implementation should not force changes or recompilation in client code.
  • You need to support several platforms, such as databases, operating systems, rendering engines or cloud providers.
  • You want to switch implementations through configuration.

Avoid it when there is only one implementation and no realistic plan for another, or when a simple Strategy covers the need.

10. Best Practices and Common Mistakes

Best practices

  • Keep the Implementor interface small and made of primitive operations. Let the abstraction combine them into high-level behavior, as mute() does with setVolume(0).
  • Inject the implementor through the constructor and store it in a final field.
  • Validate the implementor with Objects.requireNonNull so misconfiguration fails fast.
  • Combine with a Factory or Abstract Factory to create the right implementor for an environment.

Common mistakes

  • Leaking implementation details into the abstraction, for example with instanceof Tv checks, which ties the two hierarchies back together.
  • Mirroring hierarchies: creating TvRemote and RadioRemote recreates the class explosion the pattern was meant to remove.
  • Applying it too early when only one implementation exists. Introduce the bridge when the second dimension appears.

11. Conclusion

The Bridge Design Pattern keeps your code clean, flexible and scalable by separating what a class does from how it does it. Two small hierarchies connected by a single reference replace a large grid of subclasses, and each side can grow without breaking the other.

You have seen it with shapes and colors, remotes and devices, notifications and senders, reports and export formats, and in the JDK with JDBC. It is especially useful in large applications where platforms and features change often. Continue with the Flyweight Design Pattern, or revisit the structural design patterns overview.