Skip to content

Bridge Pattern: Two Things That Change, Kept Apart

Split a class that varies in two directions into two hierarchies joined by a reference, so 3 x 3 combinations need 6 classes instead of 9.

Builds on Composition vs Inheritance

Stage 3advanced2 min read

We pull the grid apart. "What we say" becomes one hierarchy. "How we send it" becomes another. A single field connects them.

Side 1: the channel (implementation)

interface MessageSender {
  void send(String to, String text);
}

class SmsSender implements MessageSender {
  public void send(String to, String text) {
    System.out.println("[SMS via gateway] to " + to + ": " + text);
  }
}

EmailSender and WhatsAppSender look the same. None of them know what an OTP or a promo is. They only deliver a string.

Side 2: the message (abstraction)

abstract class Notification {

  protected final MessageSender sender;   // this is the bridge

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

  abstract String text();

  void sendTo(String to) {
    sender.send(to, text());
  }
}

class OtpNotification extends Notification {
  ...
  String text() {
    return "Your Swiggy OTP is " + otp + ". Valid for 5 minutes.";
  }
}

Each message type only decides its text. sendTo hands the delivery to whatever sender was passed in, through the sender field. The ... in OtpNotification stands for its otp field and constructor, which the full code has.

Using it

MessageSender sms = new SmsSender();
MessageSender whatsApp = new WhatsAppSender();

new OtpNotification(sms, "482913").sendTo("+91 98450 12345");
new OtpNotification(whatsApp, "771204").sendTo("+91 98450 12345");

Same OTP class, two channels. The SMS outage fallback from the Before section is now a different argument, not a different class. The channel is chosen when the object is built.

Adding a channel

class PushSender implements MessageSender {
  public void send(String to, String text) {
    System.out.println("[Push via FCM] to " + to + ": " + text);
  }
}

One class. OTP, Promo and Order update all work with it straight away. No existing code was edited.

Before vs after

BeforeAfter
Concrete classes for 3 messages x 3 channels96 (3 + 3)
Concrete classes after adding Push127
Where the OTP text lives3 filesOtpNotification only
Where the SMS code lives3 filesSmsSender only
Changing channel at runtimePick a different classPass a different sender
Who edits whatGrowth and platform teams in the same filesGrowth owns messages, platform owns senders

What the run prints

[SMS via gateway] to +91 98450 12345: Your Swiggy OTP is 482913. Valid for 5 minutes.
[Email via SMTP] to priya@example.com: Flat 50% off up to Rs 100 on your next order. Code: TRYNEW
[WhatsApp via Business API] to +91 98450 12345: Order #SW1043 from Meghana Foods is out for delivery.
[WhatsApp via Business API] to +91 98450 12345: Your Swiggy OTP is 771204. Valid for 5 minutes.
[Push via FCM] to device-8812: Order #SW1044 from Truffles is out for delivery.
[Push via FCM] to device-8812: Flat 50% off up to Rs 100 on your next order. Code: TRYNEW

Things to keep in mind

  • Keep MessageSender small. It should only have what every channel can do. Think twice before adding sendWithSubject for email, because every other channel would then have to fake it.
  • The senders keep no data between messages, so create them once and share them (in Spring they would just be beans, objects the framework creates and shares for you).
  • If a sender wraps a vendor SDK with a different API (MSG91, Gupshup, SES), that wrapper is an Adapter. Adapter and Bridge work well together.

Run this version

Run these commands from the repository root:

mkdir -p out
javac -d out src/DesignPatterns/StructuralPatterns/BridgeDesignPattern/After/Main.java
java -cp out DesignPatterns.StructuralPatterns.BridgeDesignPattern.After.Main

The teaching example keeps all interfaces and classes in Main.java, but think of them as separate parts with separate jobs. The file count does not matter. What matters is that either axis can gain a class without multiplying or editing the other axis.

In this topic

After code1 file

After/Main.java131 linesGitHub
package DesignPatterns.StructuralPatterns.BridgeDesignPattern.After;/** * IMPLEMENTATION side: how a message physically leaves our system. * Knows nothing about OTPs, promos or orders. */interface MessageSender {  void send(String to, String text);}class SmsSender implements MessageSender {  @Override  public void send(String to, String text) {    System.out.println("[SMS via gateway] to " + to + ": " + text);  }}class EmailSender implements MessageSender {  @Override  public void send(String to, String text) {    System.out.println("[Email via SMTP] to " + to + ": " + text);  }}class WhatsAppSender implements MessageSender {  @Override  public void send(String to, String text) {    System.out.println("[WhatsApp via Business API] to " + to + ": " + text);  }}// Added later. Zero changes to any Notification class.class PushSender implements MessageSender {  @Override  public void send(String to, String text) {    System.out.println("[Push via FCM] to " + to + ": " + text);  }}/** * ABSTRACTION side: what the message says. * Holds a MessageSender. That reference is the bridge. */abstract class Notification {  protected final MessageSender sender;  protected Notification(MessageSender sender) {    this.sender = sender;  }  abstract String text();  void sendTo(String to) {    sender.send(to, text());  }}class OtpNotification extends Notification {  private final String otp;  OtpNotification(MessageSender sender, String otp) {    super(sender);    this.otp = otp;  }  @Override  String text() {    return "Your Swiggy OTP is " + otp + ". Valid for 5 minutes.";  }}class PromoNotification extends Notification {  private final String code;  PromoNotification(MessageSender sender, String code) {    super(sender);    this.code = code;  }  @Override  String text() {    return "Flat 50% off up to Rs 100 on your next order. Code: " + code;  }}class OrderUpdateNotification extends Notification {  private final String orderId;  private final String restaurant;  OrderUpdateNotification(MessageSender sender, String orderId, String restaurant) {    super(sender);    this.orderId = orderId;    this.restaurant = restaurant;  }  @Override  String text() {    return "Order #" + orderId + " from " + restaurant + " is out for delivery.";  }}public class Main {  public static void main(String[] args) {    MessageSender sms = new SmsSender();    MessageSender email = new EmailSender();    MessageSender whatsApp = new WhatsAppSender();    // Any message type with any channel, picked at runtime.    new OtpNotification(sms, "482913").sendTo("+91 98450 12345");    new PromoNotification(email, "TRYNEW").sendTo("priya@example.com");    new OrderUpdateNotification(whatsApp, "SW1043", "Meghana Foods").sendTo("+91 98450 12345");    // Moving OTP from SMS to WhatsApp is now a different argument, not a different class.    new OtpNotification(whatsApp, "771204").sendTo("+91 98450 12345");    // New channel: one class. It works with all three message types right away.    MessageSender push = new PushSender();    new OrderUpdateNotification(push, "SW1044", "Truffles").sendTo("device-8812");    new PromoNotification(push, "TRYNEW").sendTo("device-8812");  }}

Done with this topic?

How well do you know it?

PreviousMemory Optimization through Sharing
Practise 7 questions