Bridge
Decouples an abstraction from its implementation so both can vary independently — using composition instead of inheritance to avoid the combinatorial explosion of subclasses.
Intent
Decouple abstraction from implementation through composition, so that both axes can vary and evolve completely independently. The abstraction delegates to the implementor instead of inheriting from it — replacing the N×M hierarchy of subclasses with N + M classes.
Cataloged by the GoF (1994) as a structural pattern, the Bridge is the planned answer to the combinatorial explosion problem of inheritance: when a class has two (or more) independent axes of variation, creating a subclass for every combination scales unsustainably. The Bridge solves this by separating the two axes into distinct hierarchies and connecting them through composition.
Problem
Imagine a notification system with two independent axes of variation: the type (normal, urgent) and the send channel (e-mail, SMS). If you model this with pure inheritance, you need a subclass for every combination:
NormalNotificationByEmailNormalNotificationBySmsUrgentNotificationByEmailUrgentNotificationBySms
With just 2 types and 2 channels, that's already 4 classes. Adding a WhatsApp channel and a Scheduled type raises it to 3×3 = 9 classes. With 5 types and 5 channels: 25 classes. The growth is multiplicative — and any change in one dimension requires updating the other.
The Bridge eliminates the explosion: each axis has its own hierarchy.
Types and channels evolve completely independently. Adding WhatsApp is
creating a WhatsAppSender class — without touching any
notification type.
Bridge vs Adapter: the critical distinction
This is the most frequent confusion with the Bridge. The difference is in the timing and the intent:
- The Adapter is applied afterwards — you have two incompatible interfaces that already exist independently, and the Adapter reconciles them. It's a corrective solution to an existing integration problem.
- The Bridge is planned from the start — you identify two independent axes of variation in the design and intentionally separate them so they can evolve without affecting each other. It's a preventive architecture decision.
In short: Adapter reconciles existing incompatibilities; Bridge prevents coupling between dimensions of variation from the design stage.
Bridge vs Strategy
Both use composition to delegate behavior, and the code structure can be identical. The difference is conceptual:
- The Strategy is a behavioral pattern with a single axis of variation: the algorithm. The context chooses which Strategy to use and can swap it at runtime. The focus is on the interchangeability of algorithms.
- The Bridge is a structural pattern with two independent axes of variation: abstraction and implementation. Both can vary and have their own hierarchies. The focus is on separating structural dimensions of the design.
Solution
The Bridge organizes the code into four participants:
-
Abstraction: defines the high-level interface and
keeps a reference to the Implementor. Can be an abstract or
concrete class. E.g.: abstract class
Notificationwith a reference toSendChannel. -
RefinedAbstraction: extends the Abstraction, adding
behavior specific to each variant. E.g.:
NormalNotification,UrgentNotification. -
Implementor (interface): defines the contract of
the implementation axis — typically low-level operations. E.g.:
SendChannelinterface withsend(recipient, message). -
ConcreteImplementor: fulfills the Implementor
contract. E.g.:
EmailSender,SmsSender. New implementations don't affect any abstraction class.
The "bridge" is the reference the Abstraction keeps to the
Implementor. Since this reference is typed as the
SendChannel interface (not a concrete class), any
ConcreteImplementor can be injected — at construction time or at
runtime.
Structure
Abstraction axis Implementation axis
Notification (abstract) «interface» SendChannel
┌────────────────────────────────┐ ┌──────────────────────────────┐
│ # channel: SendChannel │─────────────────▶│ + send(recipient, msg): void │
│ + notify(recipient, msg): void │ └──────────────────────────────┘
└────────────────────────────────┘ ▲
▲ ┌───────────────┴───────────┐
┌────────────────┴────────────────────────────┐ EmailSender SmsSender
NormalNotification UrgentNotification
(Refined Abstraction) (Refined Abstraction)
Without Bridge — N×M explosion of subclasses:
2 types × 2 channels = 4 classes
NormalNotificationByEmail
NormalNotificationBySms
UrgentNotificationByEmail
UrgentNotificationBySms
→ with 5 types and 5 channels: 25 classes
With Bridge — N + M classes:
2 types + 2 channels = 4 classes
NormalNotification, UrgentNotification
EmailSender, SmsSender
→ with 5 types and 5 channels: only 10 classes
Call flow:
urgent.notify("ops@company.com", "Server is down!")
│
▼ (UrgentNotification.notify)
│ "[URGENT]" prefix applied
│
▼ (delegates to this.channel.send)
EmailSender.send("ops@company.com", "[URGENT] Server is down!")
│
▼
[EMAIL to ops@company.com] [URGENT] Server is down!
Code examples
Example 1 — Notifications by type and channel
The abstraction (Notification) and the implementor
(SendChannel) vary completely independently. Adding a
new channel or a new notification type doesn't require changing the
other axis.
// ── Implementor — send channel axis ────────────────────────────
interface SendChannel {
send(recipient: string, message: string): void;
}
// ── ConcreteImplementors ──────────────────────────────────────
class EmailSender implements SendChannel {
send(recipient: string, message: string): void {
console.log(`[EMAIL to ${recipient}] ${message}`);
}
}
class SmsSender implements SendChannel {
send(recipient: string, message: string): void {
console.log(`[SMS to ${recipient}] ${message}`);
}
}
// Adding WhatsAppSender doesn't affect any Notification class:
class WhatsAppSender implements SendChannel {
send(recipient: string, message: string): void {
console.log(`[WA to ${recipient}] ${message}`);
}
}
// ── Abstraction — notification type axis ───────────────────────
abstract class Notification {
// The "bridge": reference to the implementor — injected in the constructor.
constructor(protected readonly channel: SendChannel) {}
abstract notify(recipient: string, content: string): void;
}
// ── Refined abstractions ────────────────────────────────────────
class NormalNotification extends Notification {
notify(recipient: string, content: string): void {
this.channel.send(recipient, content);
}
}
class UrgentNotification extends Notification {
notify(recipient: string, content: string): void {
this.channel.send(recipient, `[URGENT] ${content}`);
}
}
// Adding ScheduledNotification doesn't affect any SendChannel class:
class ScheduledNotification extends Notification {
constructor(channel: SendChannel, private readonly scheduledFor: string) {
super(channel);
}
notify(recipient: string, content: string): void {
this.channel.send(
recipient,
`[SCHEDULED ${this.scheduledFor}] ${content}`
);
}
}
// ── Usage — free combinations without new subclasses ──────────
const email = new EmailSender();
const sms = new SmsSender();
const wa = new WhatsAppSender();
new NormalNotification(email).notify(
"ana@example.com", "Your order has been confirmed."
);
// [EMAIL to ana@example.com] Your order has been confirmed.
new UrgentNotification(email).notify(
"ops@company.com", "Server is down!"
);
// [EMAIL to ops@company.com] [URGENT] Server is down!
new UrgentNotification(sms).notify(
"+1-555-0000", "Server is down!"
);
// [SMS to +1-555-0000] [URGENT] Server is down!
new ScheduledNotification(wa, "2026-07-01 09:00").notify(
"+1-555-0001", "Meeting reminder"
);
// [WA to +1-555-0001] [SCHEDULED 2026-07-01 09:00] Meeting reminder
<?php
// ── Implementor — send channel axis ────────────────────────────
interface SendChannel
{
public function send(string $recipient, string $message): void;
}
// ── ConcreteImplementors ──────────────────────────────────────
class EmailSender implements SendChannel
{
public function send(string $recipient, string $message): void
{
echo "[EMAIL to {$recipient}] {$message}" . PHP_EOL;
}
}
class SmsSender implements SendChannel
{
public function send(string $recipient, string $message): void
{
echo "[SMS to {$recipient}] {$message}" . PHP_EOL;
}
}
// Adding WhatsAppSender doesn't affect any Notification class:
class WhatsAppSender implements SendChannel
{
public function send(string $recipient, string $message): void
{
echo "[WA to {$recipient}] {$message}" . PHP_EOL;
}
}
// ── Abstraction — notification type axis ───────────────────────
abstract class Notification
{
// The "bridge": reference to the implementor — injected in the constructor.
public function __construct(
protected readonly SendChannel $channel
) {}
abstract public function notify(string $recipient, string $content): void;
}
// ── Refined abstractions ────────────────────────────────────────
class NormalNotification extends Notification
{
public function notify(string $recipient, string $content): void
{
$this->channel->send($recipient, $content);
}
}
class UrgentNotification extends Notification
{
public function notify(string $recipient, string $content): void
{
$this->channel->send($recipient, "[URGENT] {$content}");
}
}
// Adding ScheduledNotification doesn't affect any SendChannel class:
class ScheduledNotification extends Notification
{
public function __construct(
SendChannel $channel,
private readonly string $scheduledFor
) {
parent::__construct($channel);
}
public function notify(string $recipient, string $content): void
{
$this->channel->send(
$recipient,
"[SCHEDULED {$this->scheduledFor}] {$content}"
);
}
}
// ── Usage — free combinations without new subclasses ──────────
$email = new EmailSender();
$sms = new SmsSender();
$wa = new WhatsAppSender();
(new NormalNotification($email))->notify(
'ana@example.com', 'Your order has been confirmed.'
);
// [EMAIL to ana@example.com] Your order has been confirmed.
(new UrgentNotification($email))->notify(
'ops@company.com', 'Server is down!'
);
// [EMAIL to ops@company.com] [URGENT] Server is down!
(new UrgentNotification($sms))->notify(
'+1-555-0000', 'Server is down!'
);
// [SMS to +1-555-0000] [URGENT] Server is down!
(new ScheduledNotification($wa, '2026-07-01 09:00'))->notify(
'+1-555-0001', 'Meeting reminder'
);
// [WA to +1-555-0001] [SCHEDULED 2026-07-01 09:00] Meeting reminder
Example 2 — Geometric shapes with interchangeable renderers
The classic GoF example: shapes (abstraction) vary independently from the renderer (implementation). A new shape type doesn't require any new renderer, and a new renderer doesn't require any new shape.
// ── Implementor — renderer axis ────────────────────────────────
interface Renderer {
renderShape(type: string, params: string): void;
}
class VectorRenderer implements Renderer {
renderShape(type: string, params: string): void {
console.log(`[SVG] ${type}(${params})`);
}
}
class RasterRenderer implements Renderer {
renderShape(type: string, params: string): void {
console.log(`[Canvas] ${type}(${params})`);
}
}
// ── Abstraction — geometric shape axis ──────────────────────────
abstract class Shape {
constructor(protected renderer: Renderer) {}
abstract draw(): void;
// The implementor can be swapped at runtime:
switchRenderer(renderer: Renderer): void {
this.renderer = renderer;
}
}
// ── Refined abstractions ────────────────────────────────────────
class Circle extends Shape {
constructor(renderer: Renderer, private readonly radius: number) {
super(renderer);
}
draw(): void {
this.renderer.renderShape("circle", `r=${this.radius}`);
}
}
class Rectangle extends Shape {
constructor(
renderer: Renderer,
private readonly width: number,
private readonly height: number
) {
super(renderer);
}
draw(): void {
this.renderer.renderShape(
"rectangle", `w=${this.width} h=${this.height}`
);
}
}
// ── Usage ───────────────────────────────────────────────────────
const vector = new VectorRenderer();
const raster = new RasterRenderer();
const c = new Circle(vector, 50);
c.draw();
// [SVG] circle(r=50)
new Circle(raster, 50).draw();
// [Canvas] circle(r=50)
new Rectangle(vector, 200, 100).draw();
// [SVG] rectangle(w=200 h=100)
new Rectangle(raster, 200, 100).draw();
// [Canvas] rectangle(w=200 h=100)
// Swapping the renderer at runtime — no new subclass:
c.switchRenderer(raster);
c.draw();
// [Canvas] circle(r=50)
<?php
// ── Implementor — renderer axis ────────────────────────────────
interface Renderer
{
public function renderShape(string $type, string $params): void;
}
class VectorRenderer implements Renderer
{
public function renderShape(string $type, string $params): void
{
echo "[SVG] {$type}({$params})" . PHP_EOL;
}
}
class RasterRenderer implements Renderer
{
public function renderShape(string $type, string $params): void
{
echo "[Canvas] {$type}({$params})" . PHP_EOL;
}
}
// ── Abstraction — geometric shape axis ──────────────────────────
abstract class Shape
{
public function __construct(
protected Renderer $renderer
) {}
abstract public function draw(): void;
// The implementor can be swapped at runtime:
public function switchRenderer(Renderer $renderer): void
{
$this->renderer = $renderer;
}
}
// ── Refined abstractions ────────────────────────────────────────
class Circle extends Shape
{
public function __construct(
Renderer $renderer,
private readonly int $radius
) {
parent::__construct($renderer);
}
public function draw(): void
{
$this->renderer->renderShape('circle', "r={$this->radius}");
}
}
class Rectangle extends Shape
{
public function __construct(
Renderer $renderer,
private readonly int $width,
private readonly int $height
) {
parent::__construct($renderer);
}
public function draw(): void
{
$this->renderer->renderShape(
'rectangle',
"w={$this->width} h={$this->height}"
);
}
}
// ── Usage ───────────────────────────────────────────────────────
$vector = new VectorRenderer();
$raster = new RasterRenderer();
$c = new Circle($vector, 50);
$c->draw();
// [SVG] circle(r=50)
(new Circle($raster, 50))->draw();
// [Canvas] circle(r=50)
(new Rectangle($vector, 200, 100))->draw();
// [SVG] rectangle(w=200 h=100)
(new Rectangle($raster, 200, 100))->draw();
// [Canvas] rectangle(w=200 h=100)
// Swapping the renderer at runtime — no new subclass:
$c->switchRenderer($raster);
$c->draw();
// [Canvas] circle(r=50)
When to use
- When you identify two independent axes of variation: any structure where "type of X" and "implementation of Y" vary separately and you don't want to create a subclass for every combination.
- When abstraction and implementation must be independently extensible: different teams maintain each axis; new implementations shouldn't affect the abstractions and vice versa.
- When you want to swap the implementation at runtime: since the implementor is injected through composition, it can be replaced without recreating the abstraction.
- To hide implementation details from client code: the client only knows the abstraction's interface; the ConcreteImplementors remain fully encapsulated.
When to avoid
- When there's only one axis of variation: if only the implementation varies (and not the abstraction), a simple Strategy already solves it without the Bridge's extra structure.
- When the design isn't clear yet: the Bridge requires identifying the two axes of variation ahead of time. Applying it too early to an unstable design creates premature abstraction that hinders subsequent changes.
- When the hierarchy is small and stable: if you have 2 types and 2 implementations that rarely change, 4 direct subclasses can be simpler and more readable than the Bridge's formal structure.
Pros and cons
Pros
- Eliminates the combinatorial explosion of subclasses — N + M classes instead of N × M.
- Open/Closed Principle: new abstraction or implementation types can be added without changing the other axis.
- Single Responsibility Principle: each class has a single axis of variation.
- Allows swapping the implementation at runtime through dependency injection.
- Hides implementation details from client code — only the abstraction is exposed.
Cons
- Adds structural complexity — more classes, more indirection, a steeper learning curve for whoever reads the code for the first time.
- Requires identifying the two axes of variation in the initial design — can be difficult in systems that are still evolving.
- The relationship between abstraction and implementation can be counterintuitive for developers unfamiliar with the pattern.
Common pitfalls
1. Using Bridge when Strategy would suffice
If only the implementation varies (and there's no hierarchy of abstractions), the Bridge is needlessly complex. The rule of thumb: if you have a single abstraction class and several implementors, use Strategy. If you have hierarchies on both sides, use Bridge.
2. Confusing Bridge with Adapter
Warning: the temptation to use Bridge as an Adapter is common. Remember: the Adapter reconciles two incompatible interfaces that already exist independently — it's a retroactive solution. The Bridge is designed from the start to separate two axes of variation — it's a prospective decision. If you're "adapting" an existing interface, use Adapter. If you're designing two independent axes, use Bridge.
3. Accidental coupling between the two axes
The goal of the Bridge is for abstraction and implementation to vary
independently. If the abstraction starts calling
ConcreteImplementor-specific methods (by downcasting or calling
methods beyond the Implementor interface), the
decoupling has been broken. The Implementor should expose only what
is necessary for all abstractions. If different abstractions need
different contracts from the implementor, reassess whether Bridge is
still the right pattern.
4. Not injecting the implementor — instantiating it inside the abstraction
A subtle mistake is the abstraction concretely instantiating the
implementor inside itself
(this.channel = new EmailSender()). This restores the
coupling the Bridge is meant to eliminate. The implementor should
always be injected from outside — via constructor, setter, or
Abstract Factory — so both axes remain independent and testable.
Related patterns
The Bridge is frequently compared to other patterns that also use composition to delegate behavior:
The Adapter is the pattern most confused with Bridge: Adapter reconciles existing incompatible interfaces (retroactive solution); Bridge separates two independent axes from the design stage (prospective solution). The Strategy also uses composition to delegate, but has a single axis of variation (the algorithm); Bridge has two axes with their own hierarchies. The Abstract Factory can be combined with Bridge to create the abstraction + implementor pair consistently — the factory instantiates the correct implementation and injects it into the abstraction. The Proxy also uses composition, but represents a single object and controls access to it; Bridge structurally separates two independent axes of variation.