Adapter closed the previous lesson with a promise: to stop fixing mismatches after the fact and learn to design up front so that two dimensions bound to vary never get welded together. That is exactly the Bridge pattern, and PideYa has the textbook case waiting for it: notifications. There are types of notification (order confirmation, delay alert, promotion) and there are delivery channels (push, SMS, email). Two independent axes which, modeled with naive inheritance, multiply against each other. Bridge separates them into two hierarchies connected by a bridge, so that each axis can grow without ever hearing about the other.
Contents
- The problem in PideYa: the type × channel explosion
- Intent and structure of the pattern
- Complete Java implementation
- Growing along both axes
- Bridge versus Adapter (and a mention of Strategy)
- When to use it and when not to
- Common mistakes, exercises, and conclusion
The problem in PideYa: the type × channel explosion
In module 2 we solved who creates the notifiers for each channel (PushNotifier, SmsNotifier, EmailNotifier, chosen through NotifierRegistry). But the product grew: "send a text through a channel" is no longer enough. Each notification type has its own business logic:
- Order confirmation: includes the order number, a summary of the lines, and the estimated time; always sent.
- Delay alert: computes the new estimated time, attaches an apology and, if the delay exceeds 20 minutes, a compensation coupon.
- Promotion: marketing copy with legal terms; only sent to customers who have given consent.
The team's first attempt was to extend the existing hierarchy through inheritance, one subclass per combination:
// First attempt: one subclass for EACH combination. Do NOT imitate.
public class ConfirmationPush extends PushNotifier { /* builds the confirmation text and sends it via push */ }
public class ConfirmationSms extends SmsNotifier { /* the SAME text, via SMS */ }
public class ConfirmationEmail extends EmailNotifier { /* the SAME text, via email */ }
public class DelayPush extends PushNotifier { /* ... */ }
public class DelaySms extends SmsNotifier { /* ... */ }
public class DelayEmail extends EmailNotifier { /* ... */ }
public class PromoPush extends PushNotifier { /* ... */ }
// ... and on it goesWith 3 types and 3 channels, 9 classes. Marketing asks for "rate your order" notifications (new type): +3 classes. Operations signs up WhatsApp (new channel): +4 classes (one for each existing type). The hierarchy grows as the product of the two axes: types × channels. And there is damage worse than the head count: duplication. The "when does a compensation coupon apply" logic is copied into DelayPush, DelaySms, and DelayEmail; the day the 20-minute threshold changes, three classes will need touching and somebody will forget one. DRY, dynamited.
The precise diagnosis: the single hierarchy is trying to capture two independent dimensions of variation —what is notified and through which channel it is sent— with a single mechanism, inheritance, which only knows how to grow along one axis. The solution is not more inheritance: it is splitting the hierarchy in two and connecting the halves with composition.
Intent and structure of the pattern
Intent (GoF): decouple an abstraction from its implementation so that the two can vary independently.
The GoF names are misleading, so let's translate them before the diagram:
- Abstraction: the hierarchy oriented toward the business/client; here, the notification types (what is said, when, under which rules).
- Implementation (Implementor): the hierarchy oriented toward the platform/technology; here, the delivery channels (how the message physically arrives).
- The bridge: the abstraction holds a reference to the implementor and delegates the technical part to it. That composition arrow between the two hierarchies is the entire pattern.
classDiagram
class Notification {
<<abstract>>
#channel: DeliveryChannel
+Notification(channel: DeliveryChannel)
+notify(order: Order)*
}
class ConfirmationNotification {
+notify(order: Order)
}
class DelayNotification {
-delayMinutes: int
+notify(order: Order)
}
class PromoNotification {
-campaign: Campaign
+notify(order: Order)
}
class DeliveryChannel {
<<interface>>
+send(recipient: Customer, title: String, body: String)
}
class PushChannel {
+send(recipient, title, body)
}
class SmsChannel {
+send(recipient, title, body)
}
class EmailChannel {
+send(recipient, title, body)
}
Notification <|-- ConfirmationNotification
Notification <|-- DelayNotification
Notification <|-- PromoNotification
DeliveryChannel <|.. PushChannel
DeliveryChannel <|.. SmsChannel
DeliveryChannel <|.. EmailChannel
Notification o-- DeliveryChannel : bridge
| GoF role | In PideYa |
|---|---|
| Abstraction | Notification (base class holding the channel reference) |
| RefinedAbstraction | ConfirmationNotification, DelayNotification, PromoNotification |
| Implementor | DeliveryChannel |
| ConcreteImplementor | PushChannel, SmsChannel, EmailChannel |
Back-of-the-envelope math: with Bridge, 3 types + 3 channels = 3 + 3 = 6 classes (plus one base and one interface) instead of 3 × 3 = 9. With 5 types and 4 channels: 9 versus 20. Inheritance multiplied; composition adds. And every business rule lives in exactly one class of the type hierarchy, written once.
Complete Java implementation
The implementor and its concrete channels — deliberately dumb: all they know is how to get a title and a body to a customer through their medium:
public interface DeliveryChannel {
void send(Customer recipient, String title, String body);
}
public class PushChannel implements DeliveryChannel {
@Override
public void send(Customer recipient, String title, String body) {
// Looks up the customer's device tokens and sends via FCM/APNs
EventLog.INSTANCE.info("Push to " + recipient.getId() + ": " + title);
}
}
public class SmsChannel implements DeliveryChannel {
@Override
public void send(Customer recipient, String title, String body) {
// SMS has no title: concatenate, then trim to 160 characters
String text = (title + ". " + body);
smsGateway.send(recipient.getPhone(),
text.length() <= 160 ? text : text.substring(0, 157) + "...");
}
}
public class EmailChannel implements DeliveryChannel {
@Override
public void send(Customer recipient, String title, String body) {
smtpServer.send(recipient.getEmail(), title, htmlTemplate(body));
}
}Notice that each channel handles its own technical quirks (SMS has no title and caps the character count; email lays out HTML) without ever knowing what it is sending: a confirmation? a promotion? None of its business.
The abstraction and its refinements — where the business lives:
public abstract class Notification {
protected final DeliveryChannel channel; // <- the bridge
protected Notification(DeliveryChannel channel) {
this.channel = channel;
}
/** Each type decides what to communicate and when; the HOW is delegated to the channel. */
public abstract void notify(Order order);
}
public class ConfirmationNotification extends Notification {
public ConfirmationNotification(DeliveryChannel channel) { super(channel); }
@Override
public void notify(Order order) {
String title = "Order " + order.getNumber() + " confirmed";
String body = "Your order from " + order.getRestaurant().getName()
+ " will arrive around " + order.getEstimatedTime() + ".";
channel.send(order.getCustomer(), title, body);
}
}
public class DelayNotification extends Notification {
private static final int MINUTES_FOR_COUPON = 20; // the rule, ONCE and only once
private final int delayMinutes;
public DelayNotification(DeliveryChannel channel, int delayMinutes) {
super(channel);
this.delayMinutes = delayMinutes;
}
@Override
public void notify(Order order) {
String title = "Your order is running " + delayMinutes + " minutes late";
String body = "We're sorry. New estimated time: "
+ order.getEstimatedTime().plusMinutes(delayMinutes) + ".";
if (delayMinutes > MINUTES_FOR_COUPON) {
body += " Here's a 10% coupon for your next order.";
couponService.issueCompensation(order.getCustomer());
}
channel.send(order.getCustomer(), title, body);
}
}
public class PromoNotification extends Notification {
private final Campaign campaign;
public PromoNotification(DeliveryChannel channel, Campaign campaign) {
super(channel);
this.campaign = campaign;
}
@Override
public void notify(Order order) {
Customer customer = order.getCustomer();
if (!customer.hasGivenMarketingConsent()) {
return; // the legal rule, ONCE and only once
}
channel.send(customer, campaign.getTitle(),
campaign.getText() + "\n" + campaign.getLegalTerms());
}
}The client crosses the bridge at construction time: any type with any channel, decided at runtime (by user preference, by message criticality, by channel cost):
DeliveryChannel preferredChannel = customer.prefersSms() ? new SmsChannel() : new PushChannel();
new ConfirmationNotification(preferredChannel).notify(order);
new DelayNotification(new SmsChannel(), 25).notify(order); // delays: always SMS
new PromoNotification(new EmailChannel(), campaign).notify(order); // promos: email, cheaperAll 9 combinations exist and work, yet none has a class of its own: they are compositions assembled on the fly. And creating the channels doesn't have to be a bare new: the NotifierRegistry from module 2 can be repurposed as a registry of Supplier<DeliveryChannel> and remain the single point where the channel is decided — creational patterns manufacture the ends of the bridge; Bridge decides its shape.
Growing along both axes
The acid test of the separation is the cost of growth:
- New type ("rate your order"): one class,
RatingNotification extends Notification. The channels aren't even recompiled. - New channel (WhatsApp): one class,
WhatsAppChannel implements DeliveryChannel. The types never hear about it, and automatically every existing type knows how to go out via WhatsApp.
Each axis honors OCP on its own: extension without modification, in both directions. Compare that with the multiplicative hierarchy, where the new channel cost one class per type and spread the duplicated logic a little further.
A design decision you will see in real implementations: the implementor's interface doesn't have to mirror the abstraction's. DeliveryChannel.send(recipient, title, body) is more primitive than Notification.notify(order): the abstraction works in business terms (orders, campaigns, consents) and translates into the channel's technical terms (title, body, recipient). That asymmetry is healthy; if the two interfaces were identical, you would rightly suspect that one of the two layers adds nothing.
Bridge versus Adapter (and a mention of Strategy)
Bridge and Adapter get confused because the local picture looks alike: one object delegating to another behind an interface. The difference lies in the when and the why, and it is clear-cut:
| Aspect | Adapter | Bridge |
|---|---|---|
| Timing | After the fact: the pieces already exist and don't fit | Up front: designed before the hierarchies grow |
| Problem | Incompatible interfaces you don't control | A hierarchy threatening to grow as the product of two axes |
| The interfaces involved | Foreign (adaptee) and ours (target): they get translated | Both ours: designed to collaborate |
| Flavor | Fix, customs office, honest patch | Architecture, planning for variation |
| In PideYa | PayPalAdapter for a legacy SDK |
Notification × DeliveryChannel, designed by us |
One sentence to take away: Adapter makes things that already exist fit; Bridge keeps things that are going to exist from getting welded together. In fact, they coexist happily: if tomorrow WhatsApp only offers an SDK with an alien interface, WhatsAppChannel will internally be an Adapter for that SDK... hanging off the bridge as one more implementor.
Strategy (mention only; it is developed in lesson 04-10): structurally, the bridge to DeliveryChannel is almost identical to a swappable strategy. The difference will again be one of intent —Strategy swaps algorithms inside an object; Bridge separates entire hierarchies that grow on their own— and we will sharpen it there. For now, keep this: if there were only one notification type and three ways to send it, that would be more Strategy than Bridge; the bridge earns its keep when both sides have a hierarchy.
When to use it and when not to
Use it when:
- You detect (or foresee with evidence) two independent dimensions of variation within a single responsibility: what/how, business/platform, model/render, operation/persistence.
- Subclasses start being named
SomethingSomething(ConfirmationPush,MonthlySalesPdf): the compound name betrays the two axes fused together. - You want to be able to swap the implementation at runtime (channel based on customer preferences) or hand each hierarchy to a different team.
Don't use it when:
- Only one axis varies: a normal hierarchy (or a Strategy) is enough, and the bridge would be a free-floating indirection — the over-engineering from the lesson on weighing the trade-offs.
- The "second dimension" is speculative: "maybe someday there will be more channels" with no actual assignment is textbook YAGNI. The honest symptom is an explosion already underway (those 9 classes) or a signed order for the fourth channel.
- The two hierarchies aren't truly independent: if every notification type needs intimate knowledge of every channel, the bridge becomes a sieve of
instanceofand it's better to rethink the split of responsibilities.
Relationship to other patterns (mentions only): Abstract Factory can create coherent abstraction-implementor pairs; concrete implementors can be Adapters for external SDKs; Decorator can wrap a DeliveryChannel to add retries or logging without touching the bridge; and the fine-grained comparison with Strategy and State arrives in module 4.
Common Mistakes and Tips
- An abstraction that bridges... and then peeks. If
DelayNotificationdoesif (channel instanceof SmsChannel)to shorten the text, the bridge has a hole in it: that decision (capping characters) belongs to the channel. All technical knowledge goes to the technical side. - A fat implementor. A
DeliveryChannelinterface with 15 methods (attachments, HTML, read receipts, priorities...) forces every channel to implement things it doesn't support. Keep the implementor primitive and leave the richness in the abstraction; if a subset of channels shares extra capabilities, segregate interfaces (ISP, lesson 01-03). - Mixing up the sides. Putting the consent rules in
EmailChannel"because promos go out by email". The day promos also go out by push, the legal rule is in the wrong place. Business in the abstraction, technology in the implementor, always. - A posthumous Bridge. Introducing the bridge when there are already 20 combinatorial classes is more expensive than doing it at 6, but it still pays off; do it in steps (
DeliveryChannelappears, eachTypeChannelclass delegates, the duplicates get merged) with tests in between. What doesn't pay off is introducing it when there is no second axis and never will be. - Tip: the acid test for placing a dubious piece of code is to ask "does this change if the channel changes, or if the business changes?". The coupon threshold doesn't change because you use WhatsApp: abstraction. The 160-character limit doesn't change because it's a promo: implementor.
Exercises
Exercise 1: notifications for couriers too
PideYa wants to notify couriers as well (new order assignment, zone change), and couriers only receive push or SMS. State which new classes you need and which existing ones get modified. Hint: is the recipient a new axis, or does it fit inside the existing ones?
Exercise 2: spotting the broken bridge
What is wrong here, and how would you fix it?
public class EmailChannel implements DeliveryChannel {
@Override
public void send(Customer recipient, String title, String body) {
if (title.startsWith("Order") && title.contains("confirmed")) {
body += "\nThanks for trusting PideYa."; // signature only on confirmations
}
smtpServer.send(recipient.getEmail(), title, htmlTemplate(body));
}
}Exercise 3: counting classes
The reporting team has SalesReportPdf, SalesReportExcel, DeliveryReportPdf, DeliveryReportExcel, CustomerReportPdf, and CustomerReportExcel, and orders come in for an HTML format and a promotions report. (a) How many classes will there be without Bridge once both orders are done? (b) And with Bridge (count hierarchies, base, and interface)? (c) Name the resulting GoF roles.
Solutions
Solution 1: no new axis is needed; what is needed is to generalize the recipient. Changes: (1) DeliveryChannel.send takes a common type (Recipient, an interface implemented by Customer and Courier, exposing what each channel needs: phone, tokens, email) — the only modification to existing code, and a mechanical one. (2) New classes, all refinements of the abstraction: AssignmentNotification extends Notification and ZoneChangeNotification extends Notification. The concrete channels are untouched (push and SMS already exist); that couriers don't use email is a rule for whoever composes, not a new class: you simply never build a courier notification with EmailChannel (and you can enforce it in the composing factory). Total: 2 new classes + 1 recipient interface. With the multiplicative hierarchy it would have been 4 new classes and counting.
Solution 2: the channel is sniffing the content to infer the business type (parsing the title to figure out whether it's a confirmation): business-side knowledge smuggled into the technical side, and fragile besides (it breaks when the title is translated or the copy changes). Fix: the "Thanks for trusting PideYa" signature is a decision of the notification type → it gets appended to the body in ConfirmationNotification.notify(...), and EmailChannel goes back to being dumb. If what you actually want is a signature on all emails (a genuinely technical channel decision), then it is added unconditionally in the channel, without looking at the title.
Solution 3: (a) Without Bridge: 4 reports × 3 formats = 12 combinatorial classes. (b) With Bridge: 4 refinements (SalesReport, DeliveryReport, CustomerReport, PromoReport) + 3 implementors (PdfFormat, ExcelFormat, HtmlFormat) + base Report + interface OutputFormat = 9 classes/interfaces, and above all additive future growth, not multiplicative. (c) Roles: Abstraction Report; RefinedAbstraction the four reports; Implementor OutputFormat; ConcreteImplementor the three formats.
Conclusion
Bridge splits in two a hierarchy that was growing as a product: on one side the abstraction (the notification types, with all the business), on the other the implementation (the channels, with all the technology), and between them a composition reference crossed at runtime. The result is additive growth on both axes, rules written exactly once, and teams that can evolve each side without stepping on each other. And the border with Adapter is now drawn —up-front design versus after-the-fact fix— and will return in the final comparison.
So far we have connected pieces in pairs: adapter and adaptee, abstraction and implementor. But some structures in PideYa are not pairs but trees: a restaurant's menu has sections, subsections inside them, dishes inside those... and we want to ask any node for its price or availability without first asking what it is. Treating the group the same as the individual: that recursive uniformity is the next pattern. See you in Composite.
Software Design Patterns Course
Module 1: Introduction to Design Patterns
- What Are Design Patterns?
- History and Origin of Design Patterns
- Design Principles: SOLID and Other Foundations
- Essential UML for Understanding Patterns
- Classification of Design Patterns
- Advantages and Disadvantages of Using Design Patterns
Module 2: Creational Patterns
- Introduction to Creational Patterns
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparing and Choosing Creational Patterns
Module 3: Structural Patterns
- Introduction to Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparing and Choosing Structural Patterns
Module 4: Behavioral Patterns
- Introduction to Behavioral Patterns
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparing and Choosing Behavioral Patterns
Module 5: Applying Design Patterns
- How to Select the Right Pattern
- Practical Examples of Pattern Usage
- Design Patterns in Real Projects
- Refactoring with Design Patterns
- Anti-Patterns: When Patterns Become a Problem
Module 6: Advanced Design Patterns
- Design Patterns in Modern Architectures
- Design Patterns in Microservices
- Design Patterns in Distributed Systems
- Concurrency Patterns
- Design Patterns in Agile Development
