Bridge
Bridge is a structural pattern that splits a class along two independent axes of change - what it does and how it does it - connecting the two halves through composition instead of inheritance.
Problem
A remote-control class hierarchy starts with a base `RemoteControl` and a `AdvancedRemote` subclass that adds extras like mute. Then the team needs to support multiple device types - TVs, radios, and more. Following the same inheritance approach means creating `TVBasicRemote`, `TVAdvancedRemote`, `RadioBasicRemote`, `RadioAdvancedRemote`, and so on.
Each new device type or new remote variant multiplies the existing subclasses instead of adding one class. The hierarchy is really modeling two separate concerns - the remote's control logic and the device it operates - but inheritance forces them into a single chain, so they can't vary independently.
Solution
Pull one of the two concerns out into its own hierarchy, and connect it to the original hierarchy through a held reference rather than inheritance. In the remote-control example, devices (`TV`, `Radio`) become a separate hierarchy behind a common `Device` interface. The `RemoteControl` class stops assuming a specific device and instead holds a device object, forwarding the actual power and volume operations to it.
That held reference is the bridge: remote variants and device types can now each grow their own hierarchy, and any remote - basic or advanced - can be paired with any device at runtime, without a combinatorial explosion of subclasses.
When to Use
- A class is growing subclasses along two unrelated dimensions at once - for example, message priority and delivery channel, or shape type and rendering backend.
- You expect to add new variants on either side over time and want that growth to stay linear, not multiplicative.
- You need to swap the underlying implementation at runtime - for instance, pointing the same high-level code at a live service in production and a stub in tests.
Real-World Examples
- **JDBC drivers** - application code calls the same `Connection`/`Statement` abstraction while MySQL, PostgreSQL, or SQLite each supply their own driver implementation underneath.
- **SLF4J logging facade** - code logs through one abstract API regardless of whether Logback, Log4j2, or java.util.logging ends up handling the actual output.
- **Cross-platform rendering in game engines** - a `Renderer` abstraction stays the same while separate implementations target DirectX, Vulkan, or Metal depending on the platform.
Structure
Two hierarchies sit side by side. The Abstraction side models what the client wants to do and holds a reference to an Implementation object; the Implementation side models how it actually gets done on a given platform or backend. The only connection between them is that stored reference - swapping it changes behavior without touching the Abstraction's code at all.
Participants
Exposes the high-level operations the client calls, and forwards the low-level work to whatever Implementation object it currently holds.
In the diagram: Abstraction, shown as RemoteControl.
Adds specialized operations on top of the base Abstraction without needing any changes on the Implementation side.
In the diagram: Refined Abstraction, shown as AdvancedRemote.
Defines the low-level operations available to any Abstraction - a contract that is deliberately independent from the Abstraction's own API.
In the diagram: Implementation, shown as the Device interface.
Supplies one actual backend or platform - the piece that gets swapped in or out when the Abstraction needs to target something different.
In the diagram: TV, Radio.
class Device {
isEnabled() { throw new Error('Not implemented'); }
enable() { throw new Error('Not implemented'); }
disable() { throw new Error('Not implemented'); }
getVolume() { throw new Error('Not implemented'); }
setVolume(percent) { throw new Error('Not implemented'); }
}
class TV extends Device {
constructor() {
super();
this._enabled = false;
this._volume = 30;
}
isEnabled() { return this._enabled; }
enable() { this._enabled = true; console.log('TV: powered on'); }
disable() { this._enabled = false; console.log('TV: powered off'); }
getVolume() { return this._volume; }
setVolume(percent) {
this._volume = Math.max(0, Math.min(100, percent));
console.log(`TV: volume set to ${this._volume}%`);
}
}
class Radio extends Device {
constructor() {
super();
this._enabled = false;
this._volume = 50;
}
isEnabled() { return this._enabled; }
enable() { this._enabled = true; console.log('Radio: powered on'); }
disable() { this._enabled = false; console.log('Radio: powered off'); }
getVolume() { return this._volume; }
setVolume(percent) {
this._volume = Math.max(0, Math.min(100, percent));
console.log(`Radio: volume set to ${this._volume}%`);
}
}
class RemoteControl {
constructor(device) {
this.device = device;
}
togglePower() {
if (this.device.isEnabled()) {
this.device.disable();
} else {
this.device.enable();
}
}
volumeUp() { this.device.setVolume(this.device.getVolume() + 10); }
volumeDown() { this.device.setVolume(this.device.getVolume() - 10); }
}
class AdvancedRemote extends RemoteControl {
mute() {
this.device.setVolume(0);
console.log('Remote: muted');
}
}
const tv = new TV();
const remote = new AdvancedRemote(tv);
remote.togglePower();
remote.volumeUp();
remote.mute();
const radio = new Radio();
const basicRemote = new RemoteControl(radio);
basicRemote.togglePower();
basicRemote.volumeDown();
Step 1 of 6
Device is the implementor interface
It declares `isEnabled`, `enable`, `disable`, `getVolume`, `setVolume` with no logic - any concrete device must supply all five.
Step 2 of 6
TV is one concrete implementation of Device
It keeps its own `_enabled`/`_volume` state and implements every `Device` method its own way, e.g. logging `'TV: powered on'`.
Step 3 of 6
Radio is a second, independent implementation
It implements the same `Device` interface with a different starting volume (`50`) and its own log messages - `RemoteControl` won't need to know which one it's driving.
Step 4 of 6
RemoteControl holds a Device reference, not a subclass
Its constructor stores `this.device` and every method (`togglePower`, `volumeUp`, `volumeDown`) calls back through that reference - this is the bridge between the abstraction and its implementor.
Step 5 of 6
AdvancedRemote extends the abstraction, not the device
`mute()` is new remote-side behavior built from `this.device.setVolume(0)` - the abstraction hierarchy (remotes) grows independently of the implementor hierarchy (devices).
Step 6 of 6
Any remote can drive any device
`AdvancedRemote` wraps a `TV` while a plain `RemoteControl` wraps a `Radio` - abstraction and implementor are combined freely at runtime, which is the whole point of the bridge.
interface Device {
isEnabled(): boolean;
enable(): void;
disable(): void;
getVolume(): number;
setVolume(percent: number): void;
}
class TV implements Device {
private enabled = false;
private volume = 30;
public isEnabled(): boolean { return this.enabled; }
public enable(): void { this.enabled = true; console.log('TV: powered on'); }
public disable(): void { this.enabled = false; console.log('TV: powered off'); }
public getVolume(): number { return this.volume; }
public setVolume(percent: number): void {
this.volume = Math.max(0, Math.min(100, percent));
console.log(`TV: volume set to ${this.volume}%`);
}
}
class Radio implements Device {
private enabled = false;
private volume = 50;
public isEnabled(): boolean { return this.enabled; }
public enable(): void { this.enabled = true; console.log('Radio: powered on'); }
public disable(): void { this.enabled = false; console.log('Radio: powered off'); }
public getVolume(): number { return this.volume; }
public setVolume(percent: number): void {
this.volume = Math.max(0, Math.min(100, percent));
console.log(`Radio: volume set to ${this.volume}%`);
}
}
class RemoteControl {
constructor(protected device: Device) {}
public togglePower(): void {
if (this.device.isEnabled()) {
this.device.disable();
} else {
this.device.enable();
}
}
public volumeUp(): void { this.device.setVolume(this.device.getVolume() + 10); }
public volumeDown(): void { this.device.setVolume(this.device.getVolume() - 10); }
}
class AdvancedRemote extends RemoteControl {
public mute(): void {
this.device.setVolume(0);
console.log('Remote: muted');
}
}
const tv = new TV();
const remote = new AdvancedRemote(tv);
remote.togglePower();
remote.volumeUp();
remote.mute();
const radio = new Radio();
const basicRemote = new RemoteControl(radio);
basicRemote.togglePower();
basicRemote.volumeDown();
interface Device {
boolean isEnabled();
void enable();
void disable();
int getVolume();
void setVolume(int percent);
}
class TV implements Device {
private boolean enabled = false;
private int volume = 30;
public boolean isEnabled() { return enabled; }
public void enable() { enabled = true; System.out.println("TV: powered on"); }
public void disable() { enabled = false; System.out.println("TV: powered off"); }
public int getVolume() { return volume; }
public void setVolume(int p) {
volume = Math.max(0, Math.min(100, p));
System.out.println("TV: volume set to " + volume + "%");
}
}
class Radio implements Device {
private boolean enabled = false;
private int volume = 50;
public boolean isEnabled() { return enabled; }
public void enable() { enabled = true; System.out.println("Radio: powered on"); }
public void disable() { enabled = false; System.out.println("Radio: powered off"); }
public int getVolume() { return volume; }
public void setVolume(int p) {
volume = Math.max(0, Math.min(100, p));
System.out.println("Radio: volume set to " + volume + "%");
}
}
class RemoteControl {
protected Device device;
RemoteControl(Device device) { this.device = device; }
public void togglePower() {
if (device.isEnabled()) device.disable(); else device.enable();
}
public void volumeUp() { device.setVolume(device.getVolume() + 10); }
public void volumeDown() { device.setVolume(device.getVolume() - 10); }
}
class AdvancedRemote extends RemoteControl {
AdvancedRemote(Device device) { super(device); }
public void mute() { device.setVolume(0); System.out.println("Remote: muted"); }
}
TV tv = new TV();
AdvancedRemote remote = new AdvancedRemote(tv);
remote.togglePower();
remote.volumeUp();
remote.mute();
Radio radio = new Radio();
new RemoteControl(radio).togglePower();
interface IDevice
{
bool IsEnabled();
void Enable();
void Disable();
int GetVolume();
void SetVolume(int percent);
}
class TV : IDevice
{
private bool _enabled = false;
private int _volume = 30;
public bool IsEnabled() => _enabled;
public void Enable() { _enabled = true; Console.WriteLine("TV: powered on"); }
public void Disable() { _enabled = false; Console.WriteLine("TV: powered off"); }
public int GetVolume() => _volume;
public void SetVolume(int p)
{ _volume = Math.Clamp(p, 0, 100); Console.WriteLine($"TV: volume set to {_volume}%"); }
}
class Radio : IDevice
{
private bool _enabled = false;
private int _volume = 50;
public bool IsEnabled() => _enabled;
public void Enable() { _enabled = true; Console.WriteLine("Radio: powered on"); }
public void Disable() { _enabled = false; Console.WriteLine("Radio: powered off"); }
public int GetVolume() => _volume;
public void SetVolume(int p)
{ _volume = Math.Clamp(p, 0, 100); Console.WriteLine($"Radio: volume set to {_volume}%"); }
}
class RemoteControl
{
protected readonly IDevice Device;
public RemoteControl(IDevice device) => Device = device;
public void TogglePower() { if (Device.IsEnabled()) Device.Disable(); else Device.Enable(); }
public void VolumeUp() => Device.SetVolume(Device.GetVolume() + 10);
public void VolumeDown() => Device.SetVolume(Device.GetVolume() - 10);
}
class AdvancedRemote : RemoteControl
{
public AdvancedRemote(IDevice device) : base(device) {}
public void Mute() { Device.SetVolume(0); Console.WriteLine("Remote: muted"); }
}
var tv = new TV();
var remote = new AdvancedRemote(tv);
remote.TogglePower();
remote.VolumeUp();
remote.Mute();
new RemoteControl(new Radio()).TogglePower();
from abc import ABC, abstractmethod
class Device(ABC):
@abstractmethod
def is_enabled(self) -> bool:
pass
@abstractmethod
def enable(self) -> None:
pass
@abstractmethod
def disable(self) -> None:
pass
@abstractmethod
def get_volume(self) -> int:
pass
@abstractmethod
def set_volume(self, percent: int) -> None:
pass
class TV(Device):
def __init__(self) -> None:
self._enabled = False
self._volume = 30
def is_enabled(self) -> bool: return self._enabled
def enable(self) -> None: self._enabled = True; print('TV: powered on')
def disable(self) -> None: self._enabled = False; print('TV: powered off')
def get_volume(self) -> int: return self._volume
def set_volume(self, percent: int) -> None:
self._volume = max(0, min(100, percent))
print(f'TV: volume set to {self._volume}%')
class Radio(Device):
def __init__(self) -> None:
self._enabled = False
self._volume = 50
def is_enabled(self) -> bool: return self._enabled
def enable(self) -> None: self._enabled = True; print('Radio: powered on')
def disable(self) -> None: self._enabled = False; print('Radio: powered off')
def get_volume(self) -> int: return self._volume
def set_volume(self, percent: int) -> None:
self._volume = max(0, min(100, percent))
print(f'Radio: volume set to {self._volume}%')
class RemoteControl:
def __init__(self, device: Device) -> None:
self._device = device
def toggle_power(self) -> None:
if self._device.is_enabled():
self._device.disable()
else:
self._device.enable()
def volume_up(self) -> None: self._device.set_volume(self._device.get_volume() + 10)
def volume_down(self) -> None: self._device.set_volume(self._device.get_volume() - 10)
class AdvancedRemote(RemoteControl):
def mute(self) -> None:
self._device.set_volume(0)
print('Remote: muted')
tv = TV()
remote = AdvancedRemote(tv)
remote.toggle_power()
remote.volume_up()
remote.mute()
radio = Radio()
basic_remote = RemoteControl(radio)
basic_remote.toggle_power()
basic_remote.volume_down()
Step 1 of 6
Device is the implementor interface
Every method (`is_enabled`, `enable`, `disable`, `get_volume`, `set_volume`) is `@abstractmethod` - any concrete device must implement all five before it can be instantiated.
Step 2 of 6
TV is one concrete implementation of Device
It tracks its own `_enabled`/`_volume` and implements every `Device` method, e.g. printing `'TV: powered on'` from `enable`.
Step 3 of 6
Radio is a second, independent implementation
It fulfils the same `Device` contract with its own starting volume (`50`) and messages - interchangeable with `TV` from `RemoteControl`'s point of view.
Step 4 of 6
RemoteControl holds a Device reference, not a subclass
`__init__` stores `self._device`, and `toggle_power`/`volume_up`/`volume_down` all delegate to it - this reference is the bridge between abstraction and implementor.
Step 5 of 6
AdvancedRemote extends the abstraction, not the device
`mute()` is new remote-side behavior built on `self._device.set_volume(0)` - it adds to the abstraction hierarchy without touching `Device` or its subclasses.
Step 6 of 6
Any remote can drive any device
`AdvancedRemote(tv)` and `RemoteControl(radio)` combine different abstraction/implementor pairs freely at runtime - the two hierarchies vary independently.
Click "Run" to execute this code in a sandboxed frame and see console output here.
TypeScript runs as plain JavaScript here - type annotations are stripped, not type-checked.
Click "Run" to execute this code in a sandboxed frame and see console output here.
Advantages
- The two hierarchies genuinely stop interfering with each other - adding a new device type like Radio never touches RemoteControl or AdvancedRemote code, and adding a new remote variant never touches TV or Radio code.
- New abstractions and new implementations ship as isolated additions instead of edits to a shared class.
- Keeps high-level orchestration logic and low-level platform detail in separate classes, so each stays easier to read and test.
- The implementation object can be swapped at runtime - useful for switching a real backend for a test double, or changing behavior based on configuration.
Disadvantages
- For a class with a single, stable implementation, splitting it into two hierarchies just adds indirection with no real payoff.
- Tracing a call now means jumping from the Abstraction to whatever Implementation object it currently holds, which costs some readability upfront.
- Choosing which concern belongs in the Abstraction and which in the Implementation isn't always obvious, and getting it wrong defeats the purpose.
Question 1 of 10
What two things does Bridge split a class along?
Correct! Bridge splits a class along two independent axes of change - what it does and how it does it - connecting the two halves through composition instead of inheritance.
Not quite. Bridge splits a class along two independent axes of change - what it does and how it does it - connecting the two halves through composition instead of inheritance.
Question 2 of 10
What distinguishes AdvancedRemote from RemoteControl in the participant roles?
Correct! Refined Abstraction adds specialized operations on top of the base Abstraction without needing any changes on the Implementation side - in the diagram it's shown as AdvancedRemote.
Not quite. Refined Abstraction adds specialized operations on top of the base Abstraction without needing any changes on the Implementation side - in the diagram it's shown as AdvancedRemote.
Question 3 of 10
In Bridge terminology, what role does the Device interface play?
Correct! Implementation defines the low-level operations available to any Abstraction - a contract deliberately independent from the Abstraction's own API - and in this pattern it's shown as the Device interface.
Not quite. Implementation defines the low-level operations available to any Abstraction - a contract deliberately independent from the Abstraction's own API - and in this pattern it's shown as the Device interface.
Question 4 of 10
According to the listed cons, what's the readability tradeoff Bridge introduces?
Correct! Tracing a call now means jumping from the Abstraction to whatever Implementation object it currently holds, which costs some readability upfront.
Not quite. Tracing a call now means jumping from the Abstraction to whatever Implementation object it currently holds, which costs some readability upfront.
Question 5 of 10
When is applying Bridge said to add indirection with no real payoff?
Correct! For a class with a single, stable implementation, splitting it into two hierarchies just adds indirection with no real payoff.
Not quite. For a class with a single, stable implementation, splitting it into two hierarchies just adds indirection with no real payoff.
Question 6 of 10
In the Bridge solution, how does RemoteControl relate to a Device?
Correct! RemoteControl holds a device object and forwards the actual power/volume operations to it - that held reference is the bridge.
Not quite. RemoteControl holds a device object and forwards the actual power/volume operations to it - that held reference is the bridge.
Question 7 of 10
In the walkthrough, why does AdvancedRemote's mute() count as abstraction-side growth rather than device-side growth?
Correct! mute() is new remote-side behavior built from this.device.setVolume(0) - the abstraction hierarchy grows independently of the implementor hierarchy, without touching Device or its subclasses.
Not quite. mute() is new remote-side behavior built from this.device.setVolume(0) - the abstraction hierarchy grows independently of the implementor hierarchy, without touching Device or its subclasses.
Question 8 of 10
What benefit does Bridge give regarding runtime behavior?
Correct! The implementation object can be swapped at runtime - useful for switching a real backend for a test double, or changing behavior based on configuration.
Not quite. The implementation object can be swapped at runtime - useful for switching a real backend for a test double, or changing behavior based on configuration.
Question 9 of 10
Why can't the remote/device hierarchy vary independently before Bridge is applied?
Correct! The hierarchy is really modeling two separate concerns - control logic and the device it operates - but inheritance forces them into a single chain, so they can't vary independently.
Not quite. The hierarchy is really modeling two separate concerns - control logic and the device it operates - but inheritance forces them into a single chain, so they can't vary independently.
Question 10 of 10
What happens to the remote-control hierarchy if TV and Radio support is added purely through inheritance?
Correct! Each new device type or remote variant multiplies the existing subclasses instead of adding one class, since inheritance forces two independent concerns into a single chain.
Not quite. Each new device type or remote variant multiplies the existing subclasses instead of adding one class, since inheritance forces two independent concerns into a single chain.