Back to Design Patterns
Structural Pattern

Bridge Pattern

Decouple an abstraction from its implementation so that the two can vary independently — avoiding a permanent binding between them.

What is the Bridge Pattern?

The Bridge pattern separates a class into two independent hierarchies — the abstraction (what it does) and the implementation (how it does it) — connected by a reference (the bridge).

Imagine a notification system: you have notification types (Alert, Reminder) and delivery channels (Email, SMS). Without Bridge, you'd need AlertEmail, AlertSMS, etc. With Bridge, you just compose them at runtime.

Key Idea: Use composition instead of inheritance to connect hierarchies. The abstraction delegates platform-specific work to an implementor.

The Cartesian Product Problem

Without Bridge, adding dimensions to a class hierarchy causes class count to multiply (M × N):

// Without Bridge: 3 types × 3 channels = 9 classes
class AlertEmailNotification {}
class AlertSMSNotification   {}
...

// With Bridge: 3 types + 3 senders = 6 classes
class AlertNotification { constructor(sender) {} }
class EmailSender {}
// new AlertNotification(new EmailSender()) — dynamic combo!

Core Participants

Abstraction

Abstraction

The high-level control layer. Holds a reference to an Implementor (the bridge).

Refined Abstraction

Refined Abstraction

Extends Abstraction with specific business logic while still delegating to the implementor.

Implementor

Implementor Interface

Defines the low-level operations interface for the implementation layer.

Concrete Implementor

Concrete Implementor

The actual "how"—the platform-specific code (e.g., SMTP for email, Twilio for SMS).

Implementation

// Bridge Pattern — Notification System

// ─── Implementor interface ────────────────────────────────────
interface MessageSender {
    void send(String recipient, String subject, String body);
}

// ─── Concrete Implementors ────────────────────────────────────
class EmailSender implements MessageSender {
    public void send(String recipient, String subject, String body) {
        System.out.println("[Email] To: " + recipient + " | Subject: " + subject + " | " + body);
    }
}

class SMSSender implements MessageSender {
    public void send(String recipient, String subject, String body) {
        // SMS has no subject, truncate body
        System.out.println("[SMS] To: " + recipient + " | " + body.substring(0, Math.min(160, body.length())));
    }
}

class SlackSender implements MessageSender {
    public void send(String recipient, String subject, String body) {
        System.out.println("[Slack] Channel: #" + recipient + " | *" + subject + "* " + body);
    }
}

// ─── Abstraction ──────────────────────────────────────────────
abstract class Notification {
    protected MessageSender sender;  // the bridge

    public Notification(MessageSender sender) { this.sender = sender; }

    public abstract void notify(String recipient, String message);
}

// ─── Refined Abstractions ─────────────────────────────────────
class AlertNotification extends Notification {
    public AlertNotification(MessageSender sender) { super(sender); }

    public void notify(String recipient, String message) {
        sender.send(recipient, "🚨 ALERT", "[URGENT] " + message);
    }
}

class ReminderNotification extends Notification {
    public ReminderNotification(MessageSender sender) { super(sender); }

    public void notify(String recipient, String message) {
        sender.send(recipient, "⏰ Reminder", message);
    }
}

// ─── Client ───────────────────────────────────────────────────
public class App {
    public static void main(String[] args) {
        // Mix and match abstraction × implementation freely
        Notification emailAlert   = new AlertNotification(new EmailSender());
        Notification smsAlert     = new AlertNotification(new SMSSender());
        Notification slackRemind  = new ReminderNotification(new SlackSender());

        emailAlert.notify("admin@example.com", "Server CPU at 98%!");
        smsAlert.notify("+91-9876543210",       "Server CPU at 98%!");
        slackRemind.notify("devops",            "Deployment at 3pm today");
        // 3 types × 3 senders = 9 combos — zero new classes needed!
    }
}

When to Use Bridge

Avoid Combinatorial Explosion

When a class hierarchy grows in two directions at once (e.g., FileTypes × StorageBackends).

Switch Implementations at Runtime

When you need to swap the engine without changing the interface (e.g., Payment Gateway A to B).

Hide Implementation Details

To keep client code purely business-focused, leaving technical specifics to the implementors.

Pros & Cons

Advantages

  • Eliminates M×N class explosion
  • Decouples interface from platform concerns
  • Easily test abstractions and implementations in isolation
  • Runtime flexibility

Disadvantages

  • Increased design complexity upfront
  • Can be overkill for simple class structures
  • Indirection adds one extra level of abstraction to navigate

What's Next?

Explore related structural patterns: