SSourav Saha
HomeExperienceSoftware DesignSystem DesignLearningBooksToolsContact
SSourav Saha

Building scalable backend systems, distributed infrastructure, and cloud-native applications.

Navigation

  • Home
  • Experience
  • Software Design
  • System Design
  • Learning

More

  • Books
  • Tools
  • Contact

Connect

  • LinkedIn
  • Email

© 2026 Sourav Saha. All rights reserved.

Built with using Next.js

Software DesignBridge Pattern
Software DesignStructural Patterns

Bridge Pattern

Decouple an abstraction from its implementation so both can vary independently — avoid a class explosion.

September 19, 20245 min read
design-patternsjavabridgestructural

One-line Definition

Separates an object's abstraction from its implementation, allowing both to evolve independently without exponential subclassing.


Problem

Suppose you have a Shape class with two subclasses: Circle and Square. Now you want to add colors, so you create RedCircle, BlueCircle, RedSquare, and BlueSquare. When you add a new shape (Triangle) and a new color (Green), the number of classes explodes to 3 shapes × 3 colors = 9 classes. The Bridge Pattern solves this by moving from inheritance to composition, placing the color into a separate hierarchy.


Real-world Analogy

Think of a TV remote and a TV. Samsung and Sony make different TVs. Basic and Advanced remotes exist. Without a bridge you'd need BasicSamsungRemote, AdvancedSamsungRemote, BasicSonyRemote, AdvancedSonyRemote. The bridge lets you combine any remote with any TV at runtime by composing them, so you just need 2 remote classes and 2 TV classes.


Structure

  • Client
    • Works with the Abstraction and doesn't care about the underlying Implementation.
  • Abstraction
    • High-level control layer that delegates work to the Implementation.
  • RefinedAbstraction
    • Extends the Abstraction with additional features (e.g., an Advanced Remote).
  • Implementation
    • Interface defining low-level operations.
  • ConcreteImplementation
    • Classes that implement the low-level interface (e.g., specific TVs).

Diagram

classDiagram
    class Client {
    }
    class Abstraction {
        -Implementation impl
        +operation()
    }
    class RefinedAbstraction {
        +extraOperation()
    }
    class Implementation {
        <<interface>>
        +operationImpl()
    }
    class ConcreteImplA {
        +operationImpl()
    }
    class ConcreteImplB {
        +operationImpl()
    }
    
    Client --> Abstraction
    Abstraction o-- Implementation : bridge
    RefinedAbstraction --|> Abstraction
    ConcreteImplA ..|> Implementation
    ConcreteImplB ..|> Implementation

Code Walkthrough

Notice how the Remote (Abstraction) contains a reference to a Device (Implementation). You can add new Remotes or new Devices entirely independently.

interface Device {
    void turnOn();
    void turnOff();
    void setVolume(int volume);
    int getVolume();
    void setChannel(int channel);
}

class TV implements Device {
    private int volume = 30;
    private int channel = 1;

    @Override
    public void turnOn() { System.out.println("TV is ON"); }
    @Override
    public void turnOff() { System.out.println("TV is OFF"); }
    @Override
    public void setVolume(int volume) { this.volume = volume; }
    @Override
    public int getVolume() { return volume; }
    @Override
    public void setChannel(int channel) {
        this.channel = channel;
        System.out.println("TV channel set to " + channel);
    }
}

class Remote {
    protected Device device;

    public Remote(Device device) {
        this.device = device;
    }

    public void togglePower() {
        device.turnOn();
    }

    public void volumeUp() {
        device.setVolume(device.getVolume() + 10);
        System.out.println("Volume: " + device.getVolume());
    }
}

class AdvancedRemote extends Remote {
    public AdvancedRemote(Device device) {
        super(device);
    }

    public void mute() {
        device.setVolume(0);
        System.out.println("Device muted");
    }
}

class Main {
    public static void main(String[] args) {
        Device tv = new TV();
        
        // Basic remote bridges to TV
        Remote basicRemote = new Remote(tv);
        basicRemote.togglePower();
        basicRemote.volumeUp();

        // Advanced remote bridges to TV
        AdvancedRemote advancedRemote = new AdvancedRemote(tv);
        advancedRemote.mute();
    }
}

Bad vs Good

Bad Approach

Problems

  • Class explosion grows multiplicatively (N shapes × M colors = N*M classes).
  • Hard to maintain and update.
class BasicTVRemote {
    public void turnOn() { System.out.println("TV ON"); }
}

class AdvancedTVRemote extends BasicTVRemote {
    public void mute() { System.out.println("TV muted"); }
}

class BasicRadioRemote {
    public void turnOn() { System.out.println("Radio ON"); }
}

class AdvancedRadioRemote extends BasicRadioRemote {
    public void mute() { System.out.println("Radio muted"); }
}

Better Approach

Improvements

  • Remote and Device evolve independently. Total classes grow additively (N + M).
  • New combinations are composed dynamically at runtime instead of hard-coded as subclasses.
interface Device {
    void turnOn();
    void setVolume(int volume);
    int getVolume();
}

class Remote {
    protected Device device;
    public Remote(Device device) { this.device = device; }
    public void togglePower() { device.turnOn(); }
}

class AdvancedRemote extends Remote {
    public AdvancedRemote(Device device) { super(device); }
    public void mute() { device.setVolume(0); }
}

// Any remote + any device
Remote r = new AdvancedRemote(new TV());

Pros vs Cons

ProsCons
Prevents exponential class explosionIncreases initial code complexity
Enforces the Open/Closed PrincipleRequires careful upfront architecture design
Abstraction and implementation evolve independentlyCan be overkill for a single dimension of variation
Runtime flexibility to swap implementationsIndirection may confuse readers used to inheritance
Hides implementation details from the client

When to Use

  • You want to divide and organize a monolithic class that has multiple variants of some functionality.
  • You need to extend a class in several orthogonal (independent) dimensions.
  • You want to be able to switch implementations at runtime.

When Not to Use

  • The class only varies in one dimension (e.g., just Shape, no Color).
  • The hierarchy is simple and unlikely to grow.
  • The added abstraction layer makes the code harder to read without tangible benefits.

Real-world Examples

  • java.sql.DriverManager (Abstraction) and java.sql.Driver (Implementation)
  • SLF4J (Abstraction) bridging to Logback/Log4j (Implementation)
  • GUI frameworks separating window management (Abstraction) from OS-specific rendering (Implementation)

Key Takeaway

The Bridge Pattern replaces inheritance with composition to prevent an exponential explosion of subclasses. Use it when an entity has two or more independent axes of variation (like Shape and Color, or Remote and Device). Avoid it if you're only dealing with a single dimension of variation, where simple inheritance suffices.

PreviousInterpreter PatternNextFactory Pattern