Decouple an abstraction from its implementation so that the two can vary independently, instead of multiplying into a class for every combination.
01
The problem Bridge solves
The problem
You send three kinds of notification — an alert, a reminder, a one-time passcode — over two channels, SMS and email. Six classes: AlertSms, AlertEmail, ReminderSms, and so on. Slightly silly, but manageable.
Then mobile ships and you add push. Nine classes. Then the growth team wants WhatsApp. Twelve. Then someone adds a shipping update notification, and you write four new classes for one new idea.
The maths is against you: every new notification type costs one class per channel, and every new channel costs one class per notification type. The two things change for completely unrelated reasons — a designer rewords the alert copy, an ops engineer swaps SMS vendors — but they're welded together in the same inheritance tree, so both changes land in the same twelve files.
02
How the Bridge pattern works
Stop putting two independent dimensions in one inheritance tree. Give each its own, and connect them with a reference — the bridge:
Find the two dimensions that are multiplying. Here: what the message is, and how it gets delivered.
One becomes the abstraction: the high-level thing callers use (Notification), free to grow its own subclasses.
The other becomes the implementor: an interface (Channel) with its own implementations, which knows nothing about notifications.
The abstraction holds an implementor and delegates the low-level work to it. Twelve classes become three plus four — and the next channel costs exactly one class.
"Abstraction" here does not mean abstract class. GoF uses the word in its older sense: the higher-level, policy-shaped half of the design — the part that decides what should happen. The implementor is the primitive, mechanism-shaped half that knows how. That naming is the single biggest reason people find Bridge harder than it is; read it as Policy holds Mechanism and the pattern becomes obvious.
Participants. The Abstraction (Notification) defines the high-level operation and holds an Implementor (Channel). Refined Abstractions are its subclasses; Concrete Implementors are the channel classes. Neither hierarchy knows the other's subclasses — that ignorance is what lets them grow independently.
03
See it: two dimensions, one bridge
The notification below is fixed — an OTP. Change the channel underneath it and watch the calling code stay exactly where it is. Then add a channel that didn't exist when the notification was written.
▶ Try it — swap the channel underneath
An interactive dispatcher: OtpNotification.send() calls channel.deliver() and each channel — SMS, email, push, WhatsApp — handles it, while the notification class itself is never edited. Adding a fourth channel costs one class and zero changes to any notification.
Add both extra channels and read the counter: edits to OtpNotification: 0. Now count what the same two additions would have cost in the original design — one new class per notification type each time, so with three notification types that's six classes instead of two. That ratio is the entire argument for Bridge.
04
Bridge pattern code examples
Two hierarchies, one reference between them — and a new channel that costs one class.
// ── The implementor side: how a message physically leaves the building ──
public interface Channel {
void deliver(Recipient to, String subject, String body);
}
public final class SmsChannel implements Channel {
public void deliver(Recipient to, String subject, String body) {
sms.send(to.phone(), truncate(subject + ": " + body, 160));
}
}
public final class EmailChannel implements Channel { /* … */ }
// ── The abstraction side: what the message MEANS ────────────────────────
public abstract class Notification {
protected final Channel channel; // ← the bridge, one reference
protected Notification(Channel channel) {
this.channel = channel;
}
public abstract void send(Recipient to);
}
public final class OtpNotification extends Notification {
private final String code;
public OtpNotification(Channel channel, String code) {
super(channel);
this.code = code;
}
@Override public void send(Recipient to) {
// Policy lives here: expiry wording, no-reply, never log the code.
channel.deliver(to, "Your verification code",
code + " — expires in 10 minutes. Do not share it.");
}
}
// Composed at the wiring site, and every combination is now legal for free:
Notification otp = new OtpNotification(new SmsChannel(), "481920");
// Adding WhatsAppChannel is ONE new class. No notification changes.
from typing import Protocol
class Channel(Protocol):
def deliver(self, to: Recipient, subject: str, body: str) -> None: ...
class SmsChannel:
def deliver(self, to: Recipient, subject: str, body: str) -> None:
sms.send(to.phone, f"{subject}: {body}"[:160])
class Notification:
def __init__(self, channel: Channel) -> None:
self._channel = channel # the bridge
def send(self, to: Recipient) -> None:
raise NotImplementedError
class OtpNotification(Notification):
def __init__(self, channel: Channel, code: str) -> None:
super().__init__(channel)
self._code = code
def send(self, to: Recipient) -> None:
self._channel.deliver(
to,
"Your verification code",
f"{self._code} — expires in 10 minutes. Do not share it.",
)
otp = OtpNotification(SmsChannel(), "481920")
# Python's honest alternative: with no compile-time coupling to fear, many
# codebases skip the Notification base class entirely and pass the channel to a
# plain function — `send_otp(channel, to, code)`. That IS the bridge; the
# hierarchy is optional scaffolding. Keep the class when the abstraction side
# has real state or several related operations, drop it when it doesn't.
#include <memory>
#include <string>
class Channel { // implementor
public:
virtual ~Channel() = default;
virtual void deliver(const Recipient&, std::string subject, std::string body) = 0;
};
class SmsChannel final : public Channel {
public:
void deliver(const Recipient& to, std::string subject, std::string body) override;
};
class Notification { // abstraction
public:
explicit Notification(std::shared_ptr<Channel> channel)
: channel_(std::move(channel)) {}
virtual ~Notification() = default;
virtual void send(const Recipient&) = 0;
protected:
std::shared_ptr<Channel> channel_; // the bridge
};
class OtpNotification final : public Notification {
public:
OtpNotification(std::shared_ptr<Channel> channel, std::string code)
: Notification(std::move(channel)), code_(std::move(code)) {}
void send(const Recipient& to) override {
channel_->deliver(to, "Your verification code",
code_ + " — expires in 10 minutes.");
}
private:
std::string code_;
};
// GoF's other name for this pattern is HANDLE/BODY, and C++ programmers already
// use a degenerate version of it every day: the pimpl idiom is a Bridge with
// exactly one implementor, adopted for compile-time isolation rather than for
// combinatorial growth. Same structure, different reason.
public interface IChannel
{
Task DeliverAsync(Recipient to, string subject, string body, CancellationToken ct = default);
}
public sealed class SmsChannel(ISmsClient sms) : IChannel
{
public Task DeliverAsync(Recipient to, string subject, string body, CancellationToken ct = default)
=> sms.SendAsync(to.Phone, $"{subject}: {body}"[..Math.Min(160, subject.Length + body.Length + 2)], ct);
}
public abstract class Notification(IChannel channel)
{
protected IChannel Channel { get; } = channel; // the bridge
public abstract Task SendAsync(Recipient to, CancellationToken ct = default);
}
public sealed class OtpNotification(IChannel channel, string code) : Notification(channel)
{
public override Task SendAsync(Recipient to, CancellationToken ct = default)
=> Channel.DeliverAsync(to, "Your verification code",
$"{code} — expires in 10 minutes. Do not share it.", ct);
}
// Keyed services (.NET 8+) let the container pick the implementor side at
// runtime without a factory of your own:
//
// services.AddKeyedScoped<IChannel, SmsChannel>("sms");
// services.AddKeyedScoped<IChannel, EmailChannel>("email");
//
// var channel = provider.GetRequiredKeyedService<IChannel>(user.PreferredChannel);
//
// That is the bridge assembled by configuration, which is where it belongs.
// Two independent families, joined by one reference. No classes required —
// closures express the bridge just as well.
const smsChannel = {
deliver: (to, subject, body) => sms.send(to.phone, `${subject}: ${body}`.slice(0, 160)),
}
const emailChannel = {
deliver: (to, subject, body) => smtp.send({ to: to.email, subject, html: body }),
}
const otpNotification = (channel, code) => ({
send: to =>
channel.deliver(
to,
'Your verification code',
`${code} — expires in 10 minutes. Do not share it.`,
),
})
const otp = otpNotification(smsChannel, '481920')
await otp.send(recipient)
// React is the largest Bridge most JavaScript developers use daily: your
// components are the abstraction, and the RENDERER is the implementor —
// react-dom, react-native and react-three-fiber are concrete implementors of
// the same reconciler interface. That is why one component tree can be drawn to
// a browser, a phone or a canvas without changing the components.
package notify
// Go has no inheritance, so the "two hierarchies" become two interfaces and a
// struct that embeds one — which arguably makes the pattern clearer here than
// in the languages it was written for.
type Channel interface { // implementor
Deliver(ctx context.Context, to Recipient, subject, body string) error
}
type SMS struct{ client *sms.Client }
func (s SMS) Deliver(ctx context.Context, to Recipient, subject, body string) error {
return s.client.Send(ctx, to.Phone, truncate(subject+": "+body, 160))
}
type OTP struct {
Channel // ← embedded: the bridge, and OTP now satisfies Channel too
Code string
}
func (o OTP) Send(ctx context.Context, to Recipient) error {
return o.Deliver(ctx, to, "Your verification code",
o.Code+" — expires in 10 minutes. Do not share it.")
}
// otp := OTP{Channel: SMS{client}, Code: "481920"}
//
// database/sql is the standard library's bridge and worth studying: sql.DB is
// the abstraction every Go program calls, driver.Driver is the implementor, and
// Postgres, MySQL and SQLite drivers are written by people who have never seen
// your query code.
interface Channel {
deliver(to: Recipient, subject: string, body: string): Promise<void>
}
class SmsChannel implements Channel {
async deliver(to: Recipient, subject: string, body: string): Promise<void> {
await sms.send(to.phone, `${subject}: ${body}`.slice(0, 160))
}
}
abstract class Notification {
constructor(protected readonly channel: Channel) {} // the bridge
abstract send(to: Recipient): Promise<void>
}
class OtpNotification extends Notification {
constructor(channel: Channel, private readonly code: string) {
super(channel)
}
async send(to: Recipient): Promise<void> {
await this.channel.deliver(
to,
'Your verification code',
`${this.code} — expires in 10 minutes. Do not share it.`,
)
}
}
// The type system can prove the combinatorial claim for you. This type is every
// legal pairing, and it stays a two-line definition however many of each you add:
type Send = (channel: Channel, notification: NotificationKind) => Promise<void>
// Whereas the class-per-combination design would need a union of N × M names,
// which is exactly the explosion the pattern exists to prevent.
Read across the tabs: Go's embedded interface is the cleanest statement of the pattern of the seven, because the language has no inheritance to confuse it with — and database/sql is a bridge you have already used. C++ knows the pattern under its other name, Handle/Body, and uses a one-implementor version of it (pimpl) for compile times. Python's tab is the honest one: without a compiler forcing the hierarchy, the abstraction side is often just a function that takes the channel — which is still the bridge, minus the ceremony.
05
How to implement Bridge
Identify the two dimensions that are multiplying, and check they really are independent — that they change for different reasons and at different times.
Define the implementor interface with primitive, mechanism-level operations. Keep it narrow: it should be the smallest thing every implementation can honestly do.
Give the abstraction a field holding an implementor, set through the constructor.
Put the policy — wording, sequencing, retries, business rules — in the abstraction, and the mechanism in the implementor. Getting this split wrong is what makes a bridge feel pointless.
Let each side grow its own subclasses. Neither hierarchy should ever name a class from the other.
Choose the pairing at the composition root, or from configuration, so the combination is a runtime decision rather than a compile-time one.
06
When to use Bridge — and when not to
Use it when a class hierarchy is growing along two independent axes, when you want to swap implementations at runtime, when platform-specific code should stay out of your domain classes, or when you want changes to one dimension to stop rippling into the other.
Where it goes wrong
Applying it to one dimension. If there is only ever going to be one implementor, a bridge is an interface, an indirection and a wiring line in exchange for nothing. Wait for the second implementor — it is a cheap refactor at that point, and often it never arrives.
The dimensions weren't independent. If push notifications need a title and SMS doesn't, the implementor interface grows a parameter nobody else uses, then a flag, then a downcast. When the abstraction starts asking which implementor it has, the split was wrong.
A leaky implementor interface. Every method the mechanism side exposes is a method every future implementation must provide. A twelve-method Channel makes writing the next channel a project rather than an afternoon.
Confusing it with Strategy and giving up. The structures really are near-identical. Don't agonise: if you're swapping one algorithm behind one method, call it Strategy; if you're preventing a class explosion across two growing hierarchies, call it Bridge. Nothing in the code depends on which word you use.
You want to…
Use
Because
Stop two independent hierarchies multiplying into N × M classes
Bridge
Each dimension grows on its own side of one reference.
Template Method varies by inheritance; Bridge varies by composition, at runtime.
07
Quick check
🧠 Quick check
You have 4 notification types and 5 channels. How many classes does the bridged design need, and what does adding a sixth channel cost?
That N + M versus N × M difference is the whole pattern. Without the bridge you need a class for every pairing — 20 — and each new channel adds one per notification type, so the cost of a change grows as the system grows. With the bridge you have 4 notification classes plus 5 channel classes, and the sixth channel is one file that no notification class ever hears about. The saving compounds: it isn't that 9 is smaller than 20, it's that the cost of the next change stops depending on how big the other dimension has become.
In the wild
JavaJDBC — your code calls java.sql.Connection, and every database vendor ships a Driver implementation. The abstraction and the implementors are written by different companies.
Godatabase/sql and driver.Driver — the standard library's bridge, and the reason a Postgres driver author never sees your queries.
PythonDB-API 2.0 (PEP 249) — one interface implemented by sqlite3, psycopg and mysqlclient, so ORMs target the abstraction and never a database.
C#ADO.NET providers behind DbConnection, and ILogger with its interchangeable logging providers.
JavaScriptReact's renderer split — the same component tree drawn by react-dom, react-native or a custom reconciler. Components are the abstraction; renderers are implementors.
C++Qt's platform abstraction (QPA) putting one widget API over Windows, macOS and X11 — and the pimpl idiom, a one-implementor bridge used for compile-time isolation.
What is the difference between the Bridge and Strategy patterns?
Almost nothing structurally — both are an object holding an interface it delegates to — so the difference is intent and scale. Strategy is behavioural: it swaps one interchangeable algorithm behind one operation, often several times during an object's life. Bridge is structural: it splits two hierarchies that would otherwise multiply, and the implementor is usually chosen once at construction. A practical test: if your interface has one method and several algorithms for it, that's Strategy; if both sides have their own growing family of subclasses, that's Bridge.
What does "abstraction" mean in the Bridge pattern?
Not abstract class. GoF means the higher-level half of the design — the part that defines what should happen in the caller's language ("send a one-time passcode"). The implementor is the primitive half that knows how ("put 160 characters into an SMS"). The abstraction can be a perfectly ordinary concrete class. Reading it as policy holds mechanism removes most of the confusion around this pattern.
When should I not use the Bridge pattern?
When there's only one implementation and no concrete plan for a second — you're paying an interface and an indirection for a flexibility nobody asked for, and introducing it later is a small refactor. Also skip it when the two dimensions aren't genuinely independent: if the abstraction keeps needing to know which implementor it has, the seam is in the wrong place and the bridge will leak parameters and flags until someone deletes it.
Is the pimpl idiom the Bridge pattern?
It's a degenerate case of it — one abstraction, one implementor, adopted for a completely different reason. A C++ class that holds a unique_ptr<Impl> has the Bridge's structure (GoF's alternative name, Handle/Body, describes it exactly), but the goal is compile-time isolation and ABI stability rather than preventing a class explosion. If a second implementor ever appears, it becomes a full bridge with no structural change at all.
How does Bridge differ from Adapter?
Timing and intent. Adapter is retrofitted: two things already exist, their interfaces don't match, and you write a translator. Bridge is designed in before either side has many implementations, specifically so that the interfaces never diverge and both sides can grow. Adapter fixes a problem you have; Bridge prevents one you can see coming.
Finish the handbook, earn the certificate
All 23 Gang-of-Four patterns, each with a simulator you can click and code in seven languages. Free, no signup.