Bridge Pattern explained simply
Thilan Dissanayaka Software Architecture February 15, 2020

Bridge Pattern explained simply

You have two things that can vary independently. Message types and delivery platforms. Shapes and rendering engines. Window controls and operating systems. You reach for inheritance, and suddenly your class count is multiplying like rabbits.

Two message types and three platforms? That’s six classes. Add a third message type? Nine classes. A fourth platform? Twelve. This is the class explosion problem, and it’s a sign you need the Bridge Pattern.

What is the Bridge Pattern?

The Bridge Pattern separates an abstraction from its implementation so the two can change independently. Instead of baking both dimensions into one inheritance hierarchy, you split them into two separate hierarchies connected by composition.

At its core:

  • You have an abstraction (the high-level control layer) that defines what you want to do.
  • You have an implementation (the low-level operational layer) that defines how it gets done.
  • The abstraction holds a reference to the implementation and delegates the actual work to it.
  • Both sides can be extended independently without affecting the other.

The “bridge” is that reference – the compositional link between the two hierarchies.

A Real-Life Analogy

Think about a remote control and a TV. The remote is the abstraction – it has buttons like power, volume up, channel change. The TV is the implementation – it’s the actual device that responds to those commands.

Here’s the key insight: any remote can work with any TV. A basic remote and an advanced remote with extra buttons can both control a Samsung, an LG, or a Sony. You don’t need a SamsungBasicRemote, SamsungAdvancedRemote, LGBasicRemote, LGAdvancedRemote… you just plug any remote into any TV.

That’s exactly what the Bridge Pattern does in code. It decouples “what kind of thing” from “how it works under the hood.”

Structure

Here is a UML class diagram showing how the Bridge Pattern pieces fit together for a messaging system. The Message abstraction and MessageSender implementation are two independent hierarchies connected by composition.

<mxfile>
  <diagram name="Bridge Pattern">
    <mxGraphModel dx="1100" dy="780" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="780">
      <root>
        <mxCell id="0"/>
        <mxCell id="1" parent="0"/>

        <!-- MessageSender interface (Implementation) -->
        <mxCell id="2" value="&lt;&lt;interface&gt;&gt;&#xa;MessageSender&#xa;─────────────────&#xa;+ sendMessage(title: String, body: String): void" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
          <mxGeometry x="600" y="30" width="380" height="80" as="geometry"/>
        </mxCell>

        <!-- Message abstract class (Abstraction) -->
        <mxCell id="3" value="&lt;&lt;abstract&gt;&gt;&#xa;Message&#xa;─────────────────&#xa;# sender: MessageSender&#xa;# title: String&#xa;# body: String&#xa;+ send(): void" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
          <mxGeometry x="80" y="30" width="320" height="110" as="geometry"/>
        </mxCell>

        <!-- TextMessage -->
        <mxCell id="4" value="TextMessage&#xa;─────────────────&#xa;+ send(): void" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
          <mxGeometry x="30" y="240" width="200" height="70" as="geometry"/>
        </mxCell>

        <!-- UrgentMessage -->
        <mxCell id="5" value="UrgentMessage&#xa;─────────────────&#xa;+ send(): void" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
          <mxGeometry x="270" y="240" width="200" height="70" as="geometry"/>
        </mxCell>

        <!-- EmailSender -->
        <mxCell id="6" value="EmailSender&#xa;─────────────────&#xa;+ sendMessage(title, body): void" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
          <mxGeometry x="530" y="240" width="260" height="70" as="geometry"/>
        </mxCell>

        <!-- SlackSender -->
        <mxCell id="7" value="SlackSender&#xa;─────────────────&#xa;+ sendMessage(title, body): void" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
          <mxGeometry x="530" y="340" width="260" height="70" as="geometry"/>
        </mxCell>

        <!-- SMSSender -->
        <mxCell id="8" value="SMSSender&#xa;─────────────────&#xa;+ sendMessage(title, body): void" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
          <mxGeometry x="530" y="440" width="260" height="70" as="geometry"/>
        </mxCell>

        <!-- TextMessage extends Message -->
        <mxCell id="9" style="endArrow=block;endFill=0;strokeColor=#d6b656;" edge="1" source="4" target="3" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- UrgentMessage extends Message -->
        <mxCell id="10" style="endArrow=block;endFill=0;strokeColor=#d6b656;" edge="1" source="5" target="3" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- EmailSender implements MessageSender -->
        <mxCell id="11" style="endArrow=block;dashed=1;endFill=0;strokeColor=#6c8ebf;" edge="1" source="6" target="2" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- SlackSender implements MessageSender -->
        <mxCell id="12" style="endArrow=block;dashed=1;endFill=0;strokeColor=#6c8ebf;" edge="1" source="7" target="2" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- SMSSender implements MessageSender -->
        <mxCell id="13" style="endArrow=block;dashed=1;endFill=0;strokeColor=#6c8ebf;" edge="1" source="8" target="2" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- Message has-a MessageSender (bridge) -->
        <mxCell id="14" value="uses" style="endArrow=diamond;endFill=1;strokeColor=#999999;fontColor=#999999;" edge="1" source="3" target="2" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
      </root>
    </mxGraphModel>
  </diagram>
</mxfile>

The Class Explosion Problem

Before we look at the Bridge Pattern solution, let’s see what happens without it.

Say you have a messaging system. You need to send different types of messages (text, urgent) across different platforms (email, Slack, SMS). The naive approach is one class per combination:

// Without Bridge Pattern -- one class per combination
public class TextEmailMessage { ... }
public class TextSlackMessage { ... }
public class TextSMSMessage { ... }
public class UrgentEmailMessage { ... }
public class UrgentSlackMessage { ... }
public class UrgentSMSMessage { ... }

That’s 2 message types x 3 platforms = 6 classes. Now add a ScheduledMessage type – that’s 9 classes. Add a push notification platform – that’s 12. Every new dimension multiplies the total. This is unsustainable.

The Bridge Pattern kills this problem by splitting the two dimensions into separate hierarchies. You end up with 2 + 3 = 5 classes instead of 2 x 3 = 6. And as you scale, the savings get dramatic.

Java Example: Cross-Platform Messaging System

Let’s build it properly with the Bridge Pattern.

First, the implementation interface. This is the “how” – the actual mechanism for sending a message through a specific platform:

public interface MessageSender {
    void sendMessage(String title, String body);
}

Now the concrete implementations. Each one knows how to deliver a message through its platform:

public class EmailSender implements MessageSender {
    @Override
    public void sendMessage(String title, String body) {
        System.out.println("[EMAIL] Subject: " + title);
        System.out.println("[EMAIL] Body: " + body);
        System.out.println("[EMAIL] Sent via SMTP");
    }
}

public class SlackSender implements MessageSender {
    @Override
    public void sendMessage(String title, String body) {
        System.out.println("[SLACK] *" + title + "*");
        System.out.println("[SLACK] " + body);
        System.out.println("[SLACK] Posted to #notifications channel");
    }
}

public class SMSSender implements MessageSender {
    @Override
    public void sendMessage(String title, String body) {
        System.out.println("[SMS] " + title + ": " + body);
        System.out.println("[SMS] Sent via Twilio API");
    }
}

Now the abstraction – the Message class. This is the “what” layer. It holds a reference to a MessageSender (the bridge) and defines the high-level operation:

public abstract class Message {
    protected MessageSender sender;
    protected String title;
    protected String body;

    public Message(MessageSender sender, String title, String body) {
        this.sender = sender;
        this.title = title;
        this.body = body;
    }

    public abstract void send();
}

The refined abstractions define different message behaviors. TextMessage just sends as-is:

public class TextMessage extends Message {
    public TextMessage(MessageSender sender, String title, String body) {
        super(sender, title, body);
    }

    @Override
    public void send() {
        sender.sendMessage(title, body);
    }
}

UrgentMessage adds an [URGENT] prefix to the title and sends the message twice to make sure it gets noticed:

public class UrgentMessage extends Message {
    public UrgentMessage(MessageSender sender, String title, String body) {
        super(sender, title, body);
    }

    @Override
    public void send() {
        String urgentTitle = "[URGENT] " + title;
        sender.sendMessage(urgentTitle, body);
        // Send again to make sure it's noticed
        sender.sendMessage(urgentTitle, "REMINDER: " + body);
    }
}

Now here is where the pattern shines. Mix and match any message type with any platform:

public class MainProgram {
    public static void main(String[] args) {
        // Text message via Email
        Message textEmail = new TextMessage(
                new EmailSender(), "Weekly Report", "All systems green.");
        textEmail.send();

        System.out.println("---");

        // Urgent message via Slack
        Message urgentSlack = new UrgentMessage(
                new SlackSender(), "Server Down", "Production DB unreachable!");
        urgentSlack.send();

        System.out.println("---");

        // Urgent message via SMS
        Message urgentSms = new UrgentMessage(
                new SMSSender(), "Security Alert", "Unauthorized access detected.");
        urgentSms.send();
    }
}

Output:

[EMAIL] Subject: Weekly Report
[EMAIL] Body: All systems green.
[EMAIL] Sent via SMTP
---
[SLACK] *[URGENT] Server Down*
[SLACK] Production DB unreachable!
[SLACK] Posted to #notifications channel
[SLACK] *[URGENT] Server Down*
[SLACK] REMINDER: Production DB unreachable!
[SLACK] Posted to #notifications channel
---
[SMS] [URGENT] Security Alert: Unauthorized access detected.
[SMS] Sent via Twilio API
[SMS] [URGENT] Security Alert: REMINDER: Unauthorized access detected.
[SMS] Sent via Twilio API

Need a new platform? Add one class implementing MessageSender. Need a new message type? Add one class extending Message. Neither side knows or cares about the other. No multiplication, just addition.

Key Components

  • Abstraction (Message): Defines the high-level interface and holds a reference to the implementation.
  • Refined Abstraction (TextMessage, UrgentMessage): Extends the abstraction with specific behavior.
  • Implementation (MessageSender): Defines the interface for the implementation classes.
  • Concrete Implementation (EmailSender, SlackSender, SMSSender): Provides the actual platform-specific logic.

When to Use the Bridge Pattern

  • You have two or more dimensions of variation and want to avoid a combinatorial explosion of classes.
  • You want to switch implementations at runtime. The abstraction can use a different implementation without code changes.
  • You want both the abstraction and implementation to be extensible through subclassing, independently.
  • Changes in the implementation should not affect client code that works with the abstraction.

Advantages and Disadvantages

Advantages:

  • Eliminates the class explosion problem. Class count grows by addition, not multiplication.
  • Both hierarchies can evolve independently. Add new abstractions or implementations without touching existing code.
  • Follows the Open/Closed Principle – extend without modifying.
  • Implementation details are hidden from the client. The abstraction layer provides a clean API.
  • You can swap implementations at runtime.

Disadvantages:

  • Adds complexity. For simple cases with only one dimension of variation, it’s over-engineering.
  • More indirection. Debugging requires following the delegation chain from abstraction to implementation.
  • Requires upfront design. You need to identify the two dimensions of variation early. Retrofitting the Bridge Pattern is harder than designing it in from the start.

Bridge vs Adapter

These two patterns look similar on the surface – both involve an interface and delegation. But their intent is completely different.

The Adapter Pattern fixes an incompatibility after the fact. You have two existing interfaces that don’t match, so you write an adapter to make them work together. It’s reactive.

The Bridge Pattern designs for separation upfront. You know you have two dimensions that will change independently, so you separate them from the beginning. It’s proactive.

Think of it this way: Adapter is a translator you hire because two people don’t speak the same language. Bridge is an architect who designs separate wings of a building so they can be renovated independently.

Real-World Use Cases

The Bridge Pattern shows up in many well-known systems:

  • JDBC Drivers: The java.sql API is the abstraction. Each database vendor provides their own Driver implementation. Your code talks to Connection, Statement, and ResultSet without knowing if it’s MySQL, PostgreSQL, or Oracle underneath.
  • Java AWT: The AWT toolkit uses peer classes. A java.awt.Button (abstraction) delegates to a native ButtonPeer (implementation) that’s different on Windows, macOS, and Linux.
  • Logging Frameworks: SLF4J is a classic bridge. Your code uses the SLF4J API (abstraction), and at runtime it delegates to Logback, Log4j, or java.util.logging (implementation).
  • Cross-Platform Rendering: A graphics engine might have a Shape hierarchy (circle, rectangle, triangle) and a Renderer hierarchy (OpenGL, DirectX, Vulkan). The Bridge Pattern lets any shape render on any graphics API.

Wrapping Up

The Bridge Pattern solves a specific and common problem: two dimensions of variation that would otherwise create a combinatorial explosion of classes. By splitting the abstraction from the implementation and connecting them through composition, both sides can grow independently.

The next time you find yourself creating classes like ThingAWithPlatformX, ThingAWithPlatformY, ThingBWithPlatformX… stop. That’s the class explosion smell. Pull out the Bridge Pattern, split the two dimensions, and let composition do what inheritance can’t.

ALSO READ
Writing a Shell Code for Linux
Apr 21, 2020 Exploit Development

Shellcode is a small piece of machine code used as the payload in exploit development. In this post, we write Linux shellcode from scratch — starting with a simple exit, building up to spawning a shell, and explaining every decision along the way.

Exploiting a Stack Buffer Overflow on Windows
Apr 12, 2020 Exploit Development

In a previous tutorial we discusses how we can exploit a buffer overflow vulnerability on a Linux machine. I wen through all theories in depth and explained each step. Now today we are going to jump...

Exploiting a  Stack Buffer Overflow  on Linux
Apr 01, 2020 Exploit Development

Have you ever wondered how attackers gain control over remote servers? How do they just run some exploit and compromise a computer? If we dive into the actual context, there is no magic happening....

Basic concepts of Cryptography
Mar 01, 2020 Cryptography

Ever notice that little padlock icon in your browser's address bar? That's cryptography working silently in the background, protecting everything you do online. Whether you're sending an email,...

Common Web Application Attacks
Feb 05, 2020 Application Security

Web applications are one of the most targeted surfaces by attackers. This is primarily because they are accessible over the internet, making them exposed and potentially vulnerable. Since these...

Remote Code Execution (RCE)
Jan 02, 2020 Application Security

Remote Code Execution (RCE) is the holy grail of application security vulnerabilities. It allows an attacker to execute arbitrary code on a remote server — and the consequences are as bad as it sounds. In this post, we'll go deep into RCE across multiple languages, including PHP, Java, Python, and Node.js.

> > >