Bridge has the worst official description of any pattern. "Decouple an abstraction from its implementation so that the two can vary independently" is accurate and tells you nothing about when you would want that.
The situation it addresses is concrete and recognisable: you have two independent reasons for a class to vary, and expressing both with inheritance multiplies them.
The multiplication
A notification system has kinds of notification — alert, digest, reminder — and channels — email, SMS, push. With inheritance you must pick one dimension as the base and enumerate the other:
class Notification {}
class AlertNotification extends Notification {}
class EmailAlertNotification extends AlertNotification {}
class SmsAlertNotification extends AlertNotification {}
class PushAlertNotification extends AlertNotification {}
class EmailDigestNotification extends DigestNotification {}
// 3 kinds × 3 channels = 9 classesThree by three is nine. A fourth channel makes it twelve; a fourth kind makes it sixteen. Worse, every channel's delivery logic is duplicated across every kind that uses it.
classDiagram
class Notification {
#channel: Channel
+send(user) void
#compose(user)* Content
}
class AlertNotification {
#compose(user) Content
}
class DigestNotification {
#compose(user) Content
}
class Channel {
<<interface>>
+deliver(recipient, content) void
}
class EmailChannel {
+deliver(recipient, content) void
}
class SmsChannel {
+deliver(recipient, content) void
}
Notification <|-- AlertNotification
Notification <|-- DigestNotification
Channel <|.. EmailChannel
Channel <|.. SmsChannel
Notification o-- ChannelThree kinds plus three channels is six classes rather than nine, and the gap widens as either side grows. Bridge converts a product into a sum.
class Notification {
constructor(channel) { this.channel = channel; }
send(user) { this.channel.deliver(user, this.compose(user)); }
}
class AlertNotification extends Notification {
compose(user) { return { title: 'Incident', body: this.incident.summary, urgency: 'high' }; }
}
new AlertNotification(new SmsChannel()).send(user);The distinction that actually matters
Bridge, Strategy and Adapter all end up as "an object holds another object", and teams spend more time arguing about which one they wrote than the distinction is worth. The differences with consequences:
| What varies | Why | |
|---|---|---|
| Strategy | one behaviour, swappable at runtime | the algorithm is a choice |
| Bridge | two whole hierarchies, independently | both dimensions have their own family of variants |
| Adapter | nothing — it translates | an existing interface does not fit |
The Bridge-specific signal is that both sides are hierarchies. If one side is a single interface with implementations and the other side is one class, you have Strategy. If both sides genuinely have families — several notification kinds and several channels — you have Bridge, and its contribution is preventing their product.
A second signal is timing. A strategy is usually chosen per call; a bridge is typically set once at construction and describes what the object is.
The hard part: designing the seam
Spotting the multiplication is easy. The difficulty is deciding what the implementation side is allowed to know, and this is where Bridge designs fail.
Suppose Channel.deliver(recipient, message) takes a composed string. That works until SMS needs a 160-character limit, push needs a title and a payload, and email needs a subject and an HTML alternative. The seam was designed around the easiest channel, and every real channel now needs something it cannot express.
Two ways out, with different costs:
Widen the seam. deliver(recipient, { subject, body, html, ttl }) — every channel receives everything and ignores what it does not use. Simple, and it degrades into a bag of optional fields nobody can reason about.
Move rendering to the implementation side. The notification supplies structured content — a title, a body, an urgency — and each channel renders it as it sees fit. SMS truncates, push maps urgency to a priority, email builds a subject line.
The second is usually right, and the principle generalises: the seam should carry meaning, not formatting. A seam expressed in the vocabulary of one implementation will not survive the second one.
Where it genuinely appears
Rendering across backends. A document model rendered to PDF, HTML or plain text, where document kinds also vary.
Persistence across stores. Several aggregate types persisted to several backends, where you refuse to write one class per pairing.
Platform abstraction. Several kinds of operation over several underlying platforms — the classic operating-system case, and still the clearest one.
Payment kinds over providers. One-off charges, subscriptions and refunds across several gateways: three by three, and every cell would otherwise be a class.
Check your understanding
Common mistakes
Applying it with one implementation. The seam becomes a guess shaped around the only case you have.
A seam carrying formatting instead of meaning. Pre-rendered output cannot be un-rendered by the next implementation.
Arguing about whether it is Bridge or Strategy. If both sides are hierarchies it is Bridge; otherwise the distinction is not worth a meeting.
Letting the implementation interface grow a method per implementation. That is the product returning through the back door.
Leaking implementation types through the abstraction. If a caller must know which channel it holds, the dimensions were never separated.
Using it where the dimensions are not independent. If certain kinds only work with certain implementations, the product was never full and Bridge over-generalises.
Practice
A reporting system has 4 report types and 5 output formats, implemented as 20 classes. Design the refactor, and identify what you would look for that would make you stop.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Find the seam by asking what every report type produces that every format can consume.
Show a strong answer
The refactor, once the seam is known: four report classes producing a structured result, five format classes rendering it, and a report holding a formatter. Twenty classes become nine, and — more valuable than the count — each format's logic exists once instead of being copied across four report types.
Finding the seam is the whole job. I would read all twenty classes and separate, per class, what is report logic (which rows, which aggregates, which ordering) from what is format logic (column widths, escaping, page breaks, styling). The seam is whatever the first half produces and the second half consumes — almost always a tabular structure with typed cells, headers, and a little metadata like a title and a generated-at timestamp.
The trap I would actively avoid: defining the seam as a pre-rendered string or an HTML fragment, because whichever format was written first will make that seem natural. A CSV formatter cannot un-escape HTML, and a PDF formatter cannot recover column types from text in order to right-align numbers. Semantic rows and typed cells; never formatted output.
How I would sequence it, since a 20-class rewrite in one change will not merge:
- Extract the seam type first and have one existing class produce it, with its old rendering intact. This tests the seam against a real case with nothing else changing.
- Then extract one formatter, and prove the pair reproduces byte-identical output for the existing report. Golden-file tests over the current output are the safety net here, and I would write them before touching anything.
- Then migrate the remaining cells, deleting each old class as its replacement passes.
What would make me stop — and this is the part worth being disciplined about:
- The dimensions are not independent. If PDF supports charts that CSV cannot represent, and two report types exist only as PDF, the grid was never full. A Bridge over a sparse grid forces every formatter to answer questions it has no answer to, and you get
unsupportedmethods — the product returning through the back door. - The formats need report-specific knowledge. If the PDF formatter must special-case the sales report's layout, the seam does not hold and I would be creating an abstraction that every implementation reaches around.
- The 20 classes are not really 4 × 5. Often it is 4 × 5 in the folder structure and 4 × 2 in reality, with the rest near-duplicates. If deduplicating gets it to nine classes without a pattern, that is a better outcome than a Bridge.
What I would do if I stopped: extract the shared format logic into functions the twenty classes call, which removes the duplication without asserting an independence that does not exist. The duplication was the real problem; Bridge is only one way to solve it.
Distinguish Bridge from Strategy for this case: a `PricingEngine` that can use one of three discount algorithms. Then describe the smallest change to the requirements that would turn it into a genuine Bridge.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Count the hierarchies on each side.
Show a strong answer
As stated, this is Strategy, and the counting test says so immediately. One pricing engine, three algorithms — a sum with one term on the left. There is nothing to multiply, so nothing to prevent. Calling it a Bridge would add an "abstraction hierarchy" containing one class, which is a naming exercise rather than a design.
Two further signals point the same way:
- The choice is per call. A customer's tier or a campaign decides the discount for this basket, not for the lifetime of the engine. Bridges are usually fixed at construction and describe what the object is.
- The engine has no variants of its own. There is no
SubscriptionPricingEngineneeding all three algorithms too.
The smallest change that makes it a genuine Bridge: give the left side its own family.
Suppose pricing must now work for one-off baskets, subscriptions and marketplace orders. Each computes its base differently — a subscription prorates across a billing period, a marketplace order splits across sellers before any discount applies — and each must support all three discount algorithms. Now it is 3 × 3, and inheritance over both gives nine classes with each discount implemented three times.
What that change does to the design, and why it is more than renaming:
- The seam becomes load-bearing. With Strategy the discount can take a basket. With three pricing kinds it must take something all three can produce and all three discounts can consume — a normalised set of priced line items with a currency and a period, not a basket. Getting that wrong is the failure described earlier in this topic.
- The independence has to be real. If the marketplace kind cannot use the loyalty discount, the grid is sparse and Bridge is the wrong tool — back to the stopping condition from the previous question.
The general lesson: the question "Strategy or Bridge?" is answered by counting hierarchies, not by how the code looks. Both are an object holding another object; only one of them is preventing a multiplication.
Key takeaways
- Bridge separates two independently varying dimensions, converting a product of classes into a sum.
- The countable test: if inheritance over both dimensions gives kinds × implementations, Bridge is the fix.
- Distinguish from Strategy (one varying behaviour, chosen per call) and Adapter (translation, no variation).
- The hard part is the seam. It must carry meaning, not formatting, or the second implementation cannot use it.
- Do not introduce it before both dimensions have real variants — a seam designed against one implementation is a guess, and usually a wrong one.
- If the dimensions are not fully independent, the grid is sparse, and Bridge forces implementations to answer questions they have no answer to.
- Sometimes the real problem is duplication, not a missing abstraction — deduplicate first and see what is left.
Further reading
- Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).
- Frank Buschmann, Regine Meunier, Hans Rohnert, Peter Sommerlad and Michael Stal, Pattern-Oriented Software Architecture, Volume 1 (Wiley, 1996).
- Sandi Metz, Practical Object-Oriented Design in Ruby (Addison-Wesley, 2012) — choosing what to depend on.
- Martin Fowler, Refactoring, second edition (Addison-Wesley, 2018) — replacing a subclass explosion with delegation.
- Barbara Liskov and John Guttag, Program Development in Java (Addison-Wesley, 2000) — abstraction boundaries and what they must preserve.