What is it?
Bridge fixes one specific mess. One idea changes in two independent directions, but inheritance has squeezed both directions into one class hierarchy.
"Independent" is the key word. It means you can change one direction without having to change the other:
- the message axis can gain
RateYourOrderNotificationwhile the available channels stay the same; - the channel axis can gain
PushSenderwhile the available message types stay the same.
Say every OTP must use SMS and every promo must use email. Then the choices are not independent, so there is no grid of combinations to bridge.
Bridge pulls that grid apart. One direction becomes the abstraction hierarchy. This is the side the caller uses, and it says what should happen. The other direction becomes the implementation hierarchy. This is the low-level side, and it says how it happens. The abstraction keeps a reference to the implementation interface. That reference is the "bridge".
Both sides can now grow on their own. Add a new "what" without editing any "how", and add a new "how" without editing any "what".
Be careful with the names. "Implementation" here does not mean Java's implements keyword. "Abstraction" does not always mean an abstract class. They are just names for the two roles in the pattern. In this example, message kinds such as OTP and promo are the abstraction side. Delivery channels such as SMS and email are the implementation side.
Real-life analogy
Think of a TV remote and a TV. Sony, Samsung and LG all make TVs. You can have a basic remote, a smart remote with voice, or the remote inside the Google Home app.
Nobody builds a "SonyVoiceRemote", a "SamsungVoiceRemote" and a "LGVoiceRemote". Every remote sends the same basic signals (power, volume, channel), and every TV understands them. So any remote works with any TV. Different teams design remotes and TVs, on different schedules.
The remote is the abstraction. The TV is the implementation. The IR signal is the bridge.
The problem
Say you work on notifications at Swiggy. You send three kinds of message:
- OTP at login
- Promo ("Flat 50% off up to ₹100")
- Order update ("Your order is out for delivery")
And you send them over three channels: SMS, Email and WhatsApp.
If you model this with plain inheritance, every pair becomes its own class:
OtpSms OtpEmail OtpWhatsApp
PromoSms PromoEmail PromoWhatsApp
OrderSms OrderEmail OrderWhatsApp
That is 3 x 3 = 9 classes. Product asks for push notifications, and you write 3 more (12). Marketing wants a "Rate your order" message, and you write 4 more (16). The count is messages x channels, and it only goes up.
The real pain is not the count though:
- The SMS sending code is copied into 3 classes. If the SMS gateway changes (say, you move to another provider), you edit all 3.
- The OTP text is copied into 3 classes. Change "5 minutes" to "10 minutes" and you will miss one.
- You cannot move OTP from SMS to WhatsApp while the app runs (useful when the SMS provider is down) without picking a different class.
Class diagram
Two small hierarchies instead of one big grid. 3 + 3 = 6 concrete classes (plus the abstract Notification and the MessageSender interface). Push makes it 7, not 12.
Components
| Part | In our example | Job |
|---|---|---|
| Abstraction | Notification | What the caller uses. Holds a MessageSender. |
| Refined abstraction | OtpNotification, PromoNotification, OrderUpdateNotification | One kind of message each (a variation of the "what"). Each builds its own text. |
| Implementor | MessageSender | The low level interface. Only knows how to deliver a string. |
| Concrete implementor | SmsSender, EmailSender, WhatsAppSender | One class per channel. |
| Client | Main | Picks a message type and a channel, then calls sendTo. |
One call, step by step
Read it top to bottom. The client builds an OTP with the WhatsApp sender and calls sendTo. The OTP builds its text and hands it to the sender. The OTP class never knows the channel is WhatsApp. The WhatsApp class never knows the message is an OTP.
How it fixes each problem
| Problem | With Bridge |
|---|---|
| 9 classes, growing by multiplication (m x n) | 6 classes, growing by addition (m + n) |
| SMS code copied 3 times | Lives once, in SmsSender |
| OTP text copied 3 times | Lives once, in OtpNotification |
| Channel fixed at compile time | Channel is a constructor argument, so you can pick it at runtime |
| New channel touches every message type | New channel is one class. Every message works with it right away. |
Bridge vs Adapter vs Strategy
These three look almost the same in code: a class holding a reference to an interface. The difference is why it is there.
| Bridge | Adapter | Strategy | |
|---|---|---|---|
| Intent | Let two hierarchies grow apart | Make an existing class fit an interface it was not built for | Swap one algorithm for another |
| When you add it | When the second axis shows up (the textbook says to plan it up front) | After the fact, to glue things together | When one step has many ways of doing it |
| How many hierarchies | Two, and both have subclasses | Usually one wrapper around one class | One family of algorithms. The context usually has no subclasses. |
| Example | Notification x MessageSender | Wrapping a legacy SMS vendor SDK so it looks like MessageSender | Choosing nearest driver vs surge priority in Uber matching |
A good one-liner for interviews: Adapter fixes a mismatch that already exists. Bridge stops the mismatch from ever forming. Strategy is about behaviour, Bridge is about structure.
You will often use them together. Your SmsSender might be an adapter around the Twilio or MSG91 SDK, (an SDK is the vendor's code library), sitting on the implementation side of a bridge.
Real-world places you've seen it
- JDBC. This is Bridge-like rather than textbook. Your code uses
DriverManagerand thejava.sqlinterfaces (Connection,Statement,ResultSet). Each vendor (MySQL, PostgreSQL, Oracle) ships ajava.sql.Driverthat does the real work.DriverManagerpicks one at runtime from the JDBC URL. Different people write your data access code and the drivers, and release them on different schedules. That is exactly the split Bridge is after. - java.util.logging. A
Loggerdecides what gets logged and at what level. AHandler(ConsoleHandler,FileHandler,SocketHandler) decides where it goes. Any logger can be given any handler. - AWT (Java's original window toolkit). Each
java.awt.Componentis backed by a platform "peer" (the interfaces live injava.awt.peer). The peer draws the real Windows, Mac or Linux widget. SameButtonclass, different native peer. This is close to the example the original Gang of Four book used. - Payment flows. "Refund", "Payout" and "Collect" on one side, Razorpay, PayU and Cashfree on the other. Same shape as the notification grid.
Pros and cons
Pros
- Stops the class explosion. Classes grow as
m + n, notm x n. - Each side can change, be tested and be released on its own.
- Follows Open/Closed (add new code, do not edit old code). A new message type or a new channel needs no edits to existing classes.
- The implementation can be swapped at runtime.
Cons
- More indirection (extra hops between classes). A reader has to jump between two hierarchies to follow one call.
- Overkill if you only have one dimension of change. Then it is just an interface with extra steps.
- You have to choose the split line well. A wrong split is painful to undo.
- Debugging gets one level harder. A stack trace (the list of calls that led to the error) now goes through the abstract class, the subclass and the sender before you reach the code that failed.
When to use
- You can name two independent axes, and you can see both of them growing. "Message type x channel", "shape x renderer", "report x export format".
- You keep copying the same low level code into sibling subclasses.
- You want to choose the implementation at runtime or from config.
When not to use
- Only one thing varies. Use a plain interface, or Strategy.
- The two axes are not really independent (for example, OTP must only ever go over SMS for compliance). Forcing a bridge then invites invalid combinations.
- You are integrating one third party class that has the wrong method names. That is an Adapter job.
A practical rule: do not design a Bridge on day one because you might get a second axis. Start with one interface. When the second axis actually arrives and you catch yourself writing OtpSms next to OtpEmail, that is the moment to split. Refactoring into Bridge at that point is cheap. Living for two years with a Bridge built on a guess, where one side only ever has one class, is not.
Common mistakes
- Leaking one side into the other. If
SmsSenderstarts checkingif (notification instanceof OtpNotification), the bridge is broken (instanceofchecks the type of an object). The implementor should only get plain data (to,text). - Making the implementor interface too big.
MessageSendershould only have the few basic operations every channel can do. Channel specific extras (WhatsApp templates, email subject lines) need careful design. Do not add a method that three channels answer by throwingUnsupportedOperationException. - Calling any "has-a interface" design a Bridge. If there is no second hierarchy on the abstraction side, it is just composition (one object holding another) or Strategy.
- Creating a sender per message. Senders are usually stateless (they keep no data between calls). Build them once and share them.
Interview questions
"What problem does the Bridge pattern solve?"
It stops a class explosion when something varies in two independent directions. Instead of one subclass per combination (m x n classes), you split it into two hierarchies joined by a reference, so you need only m + n classes.
"What exactly is the 'bridge' in the Bridge pattern?"
It is the field in the abstraction that points to the implementor interface, like
protected final MessageSender senderinsideNotification. Every call from the abstraction goes across that reference.
"How is Bridge different from Adapter?"
Adapter is applied after the fact to make an existing class fit an interface it was not designed for. Bridge is designed up front so two hierarchies can grow independently. Adapter fixes a mismatch, Bridge prevents one.
"Bridge and Strategy have the same class diagram. How do you tell them apart?"
By intent. Strategy swaps one algorithm inside a context, and the context usually has no subclasses. Bridge has subclasses on both sides and is about separating two dimensions of a structure, not choosing an algorithm.
"Give a real example of Bridge from the JDK."
JDBC is the usual answer. Application code is written against
ConnectionandStatement,DriverManagerpicks a vendor'sDriverat runtime from the URL, and the driver supplies the real classes. It is Bridge-like rather than a textbook match, so say that. AWT's component peers are the closer textbook example, andjava.util.logging(whereLoggerandHandlervary independently) is another good one.
"When would you not use Bridge?"
When only one thing varies, or when the two dimensions are not really independent. In both cases the extra hierarchy adds indirection without saving any classes.
"Why prefer composition over inheritance here?"
Inheritance fixes the pairing at compile time and multiplies the class count. Composition lets you pass any channel to any message at runtime, so a WhatsApp fallback during an SMS outage is a one line change.
Run both examples
From the repository root, compile and run each version separately:
mkdir -p out
javac -d out src/DesignPatterns/StructuralPatterns/BridgeDesignPattern/Before/Main.java
java -cp out DesignPatterns.StructuralPatterns.BridgeDesignPattern.Before.Main
javac -d out src/DesignPatterns/StructuralPatterns/BridgeDesignPattern/After/Main.java
java -cp out DesignPatterns.StructuralPatterns.BridgeDesignPattern.After.MainThe first three output lines are identical in both versions. The fourth differs only in the OTP (482913 before, 771204 after), and the After run adds two Push lines. The After version changes the structure so message types and channels can evolve independently.