Skip to content
DevPedia

Structural patternsGuide 10 of 28

Bridge

How to separate an abstraction from its implementation so both can evolve without creating a subclass for every combination.

Updated 7 min read

Bridge is a structural pattern that separates an abstraction from its implementation — here, the kind of notification and the delivery channel — so both can evolve separately.

The problem

AndesShop sends different kinds of notification — purchase confirmation, dispatch notice, promotional offer — and each one can be delivered through different channels — email, SMS, WhatsApp. Modeling this with direct inheritance (OrderConfirmationEmail, OrderConfirmationSms, ShippingUpdateEmail, ShippingUpdateSms, ShippingUpdateWhatsapp…) grows combinatorially: every new kind of notification is multiplied by every existing channel, and every new channel by every existing kind of notification.

The solution

Bridge separates the two dimensions that vary — the kind of notification and the delivery channel — into two independent class hierarchies, connected by a reference (the “bridge”). The notification hierarchy delegates the actual sending to a channel object, instead of implementing the sending itself.

The structure of Bridge: two independent hierarchies joined by a reference. Adding a channel doesn't touch the notifications, and vice versa.

Example in Java

// The implementation: the different delivery channels
interface DeliveryChannel {
    void deliver(String message);
}

class EmailChannel implements DeliveryChannel {
    public void deliver(String message) {
        System.out.println("Email: " + message);
    }
}

class SmsChannel implements DeliveryChannel {
    public void deliver(String message) {
        System.out.println("SMS: " + message);
    }
}

class WhatsappChannel implements DeliveryChannel {
    public void deliver(String message) {
        System.out.println("WhatsApp: " + message);
    }
}

// The abstraction: the kinds of notification, which delegate sending to the channel
abstract class Notification {
    protected final DeliveryChannel channel;

    protected Notification(DeliveryChannel channel) {
        this.channel = channel;
    }

    public abstract void send();
}

class OrderConfirmation extends Notification {
    private final String orderId;

    public OrderConfirmation(DeliveryChannel channel, String orderId) {
        super(channel);
        this.orderId = orderId;
    }

    @Override
    public void send() {
        channel.deliver("Your order " + orderId + " has been confirmed");
    }
}

class ShippingUpdate extends Notification {
    private final String trackingCode;

    public ShippingUpdate(DeliveryChannel channel, String trackingCode) {
        super(channel);
        this.trackingCode = trackingCode;
    }

    @Override
    public void send() {
        channel.deliver("Your order is on its way. Tracking: " + trackingCode);
    }
}
// Any kind of notification with any channel, without multiplying classes:
// 2 kinds x 3 channels is 5 classes, not 6. With 5 kinds and 4 channels it'd be 9, not 20.
new OrderConfirmation(new SmsChannel(), "AND-1042").send();
new ShippingUpdate(new WhatsappChannel(), "TRK-88192").send();

When to use it

  • When you have two independent dimensions that vary (kind and channel, shape and platform, content and output format) and you notice direct inheritance producing a class hierarchy that grows multiplicatively.
  • When you want to be able to change the implementation at runtime without recompiling or touching the abstraction.

When to avoid it

A single dimension that varies (notifications always by email, say, with no other channels planned) doesn’t justify Bridge: you never get to take advantage of the indirection it adds.

Benefits and drawbacks

BenefitsDrawbacks
Lets abstraction and implementation evolve separatelyIntroduces an extra layer of indirection that can make the code harder to read
Avoids combinatorial subclass growth: channel and kind live in separate hierarchies, joined by a referenceIt can be unnecessary complexity if there’s only one real implementation
Makes it easy to change the implementation at runtime

Relationship with other patterns

  • It’s usually designed up front, unlike Adapter, which is applied afterwards, over interfaces that already existed separately.
  • It can be combined with Abstract Factory: the factory decides which concrete implementation to connect to the bridge.

Share this guide

Search by concept, pattern or practice.