You’re building a drawing application. You have shapes: Circle, Square, Triangle. Each shape can be rendered differently: SVG, Canvas, PDF. With inheritance, you’d need CircleSVG, CircleCanvas, CirclePDF, SquareSVG, SquareCanvas… that’s 9 classes for 3 shapes and 3 renderers.
Bridge avoids this explosion by separating shape logic from rendering logic.
What is the Bridge Pattern?
Bridge decouples an abstraction from its implementation so they can vary independently. The abstraction contains a reference to an implementation object and delegates work to it.

The Abstraction uses the Implementor. Both sides can be extended independently.
When to Use Bridge
| Use Bridge When | Skip Bridge When |
|---|---|
| You want to avoid class explosion | Combinations are few and stable |
| Both abstraction and implementation should be extensible | One side is fixed |
| Implementation can be switched at runtime | Binding is permanent |
| You want to hide implementation from clients | Implementation details don’t matter |
Bridge is about planned decoupling, not retrofitting.
Implementation
Shapes and Renderers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// Implementation interface
public interface Renderer {
void renderCircle(double x, double y, double radius);
void renderSquare(double x, double y, double side);
void renderTriangle(double x1, double y1, double x2, double y2, double x3, double y3);
}
// Concrete implementations
public class SvgRenderer implements Renderer {
@Override
public void renderCircle(double x, double y, double radius) {
System.out.printf("<circle cx=\"%.1f\" cy=\"%.1f\" r=\"%.1f\"/>%n", x, y, radius);
}
@Override
public void renderSquare(double x, double y, double side) {
System.out.printf("<rect x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\"/>%n",
x, y, side, side);
}
@Override
public void renderTriangle(double x1, double y1, double x2, double y2, double x3, double y3) {
System.out.printf("<polygon points=\"%.1f,%.1f %.1f,%.1f %.1f,%.1f\"/>%n",
x1, y1, x2, y2, x3, y3);
}
}
public class CanvasRenderer implements Renderer {
@Override
public void renderCircle(double x, double y, double radius) {
System.out.printf("ctx.arc(%.1f, %.1f, %.1f, 0, 2 * Math.PI);%n", x, y, radius);
System.out.println("ctx.stroke();");
}
@Override
public void renderSquare(double x, double y, double side) {
System.out.printf("ctx.rect(%.1f, %.1f, %.1f, %.1f);%n", x, y, side, side);
System.out.println("ctx.stroke();");
}
@Override
public void renderTriangle(double x1, double y1, double x2, double y2, double x3, double y3) {
System.out.println("ctx.beginPath();");
System.out.printf("ctx.moveTo(%.1f, %.1f);%n", x1, y1);
System.out.printf("ctx.lineTo(%.1f, %.1f);%n", x2, y2);
System.out.printf("ctx.lineTo(%.1f, %.1f);%n", x3, y3);
System.out.println("ctx.closePath();");
System.out.println("ctx.stroke();");
}
}
// Abstraction
public abstract class Shape {
protected Renderer renderer;
public Shape(Renderer renderer) {
this.renderer = renderer;
}
public abstract void draw();
public abstract void resize(double factor);
}
// Refined abstractions
public class Circle extends Shape {
private double x, y, radius;
public Circle(double x, double y, double radius, Renderer renderer) {
super(renderer);
this.x = x;
this.y = y;
this.radius = radius;
}
@Override
public void draw() {
renderer.renderCircle(x, y, radius);
}
@Override
public void resize(double factor) {
radius *= factor;
}
}
public class Square extends Shape {
private double x, y, side;
public Square(double x, double y, double side, Renderer renderer) {
super(renderer);
this.x = x;
this.y = y;
this.side = side;
}
@Override
public void draw() {
renderer.renderSquare(x, y, side);
}
@Override
public void resize(double factor) {
side *= factor;
}
}
Usage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SVG rendering
Renderer svgRenderer = new SvgRenderer();
Shape circle = new Circle(100, 100, 50, svgRenderer);
Shape square = new Square(200, 200, 100, svgRenderer);
circle.draw();
// <circle cx="100.0" cy="100.0" r="50.0"/>
square.draw();
// <rect x="200.0" y="200.0" width="100.0" height="100.0"/>
// Same shapes, different renderer
Renderer canvasRenderer = new CanvasRenderer();
Shape circle2 = new Circle(100, 100, 50, canvasRenderer);
circle2.draw();
// ctx.arc(100.0, 100.0, 50.0, 0, 2 * Math.PI);
// ctx.stroke();
Device and Remote Control Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// Implementation - different devices
public interface Device {
boolean isEnabled();
void enable();
void disable();
int getVolume();
void setVolume(int volume);
int getChannel();
void setChannel(int channel);
}
public class Television implements Device {
private boolean on = false;
private int volume = 30;
private int channel = 1;
@Override
public boolean isEnabled() { return on; }
@Override
public void enable() {
on = true;
System.out.println("TV is ON");
}
@Override
public void disable() {
on = false;
System.out.println("TV is OFF");
}
@Override
public int getVolume() { return volume; }
@Override
public void setVolume(int volume) {
this.volume = Math.max(0, Math.min(100, volume));
System.out.println("TV volume: " + this.volume);
}
@Override
public int getChannel() { return channel; }
@Override
public void setChannel(int channel) {
this.channel = channel;
System.out.println("TV channel: " + this.channel);
}
}
public class Radio implements Device {
private boolean on = false;
private int volume = 20;
private int channel = 88; // FM frequency
@Override
public boolean isEnabled() { return on; }
@Override
public void enable() {
on = true;
System.out.println("Radio is ON");
}
@Override
public void disable() {
on = false;
System.out.println("Radio is OFF");
}
@Override
public int getVolume() { return volume; }
@Override
public void setVolume(int volume) {
this.volume = Math.max(0, Math.min(100, volume));
System.out.println("Radio volume: " + this.volume);
}
@Override
public int getChannel() { return channel; }
@Override
public void setChannel(int channel) {
this.channel = channel;
System.out.println("Radio frequency: " + this.channel + " FM");
}
}
// Abstraction - remote controls
public class RemoteControl {
protected Device device;
public 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);
}
public void channelUp() {
device.setChannel(device.getChannel() + 1);
}
public void channelDown() {
device.setChannel(device.getChannel() - 1);
}
}
// Refined abstraction - advanced remote
public class AdvancedRemote extends RemoteControl {
public AdvancedRemote(Device device) {
super(device);
}
public void mute() {
device.setVolume(0);
}
public void setChannel(int channel) {
device.setChannel(channel);
}
}
// Usage
Device tv = new Television();
RemoteControl remote = new RemoteControl(tv);
remote.togglePower(); // TV is ON
remote.volumeUp(); // TV volume: 40
remote.channelUp(); // TV channel: 2
Device radio = new Radio();
AdvancedRemote advancedRemote = new AdvancedRemote(radio);
advancedRemote.togglePower(); // Radio is ON
advancedRemote.setChannel(102); // Radio frequency: 102 FM
advancedRemote.mute(); // Radio volume: 0
How It Works
classDiagram
class RemoteControl {
#device: Device
+togglePower()
+volumeUp()
+volumeDown()
}
class AdvancedRemote {
+mute()
+setChannel()
}
class Device {
<<interface>>
+enable()
+disable()
+setVolume()
}
class Television {
+enable()
+disable()
+setVolume()
}
class Radio {
+enable()
+disable()
+setVolume()
}
RemoteControl o-- Device
RemoteControl <|-- AdvancedRemote
Device <|.. Television
Device <|.. Radio
Without Bridge: BasicTVRemote, AdvancedTVRemote, BasicRadioRemote, AdvancedRadioRemote… (2 remotes x N devices = 2N classes)
With Bridge: 2 remote classes + N device classes = N+2 classes
Common Mistakes
1. Bridge vs Adapter Confusion
1
2
3
4
5
6
7
8
9
10
11
12
// ADAPTER - makes existing incompatible interfaces work
// You have a Square peg and a Round hole
public class SquarePegAdapter implements RoundPeg {
private SquarePeg peg;
// Adapts interface
}
// BRIDGE - designed upfront for decoupling
// You plan for multiple shapes AND multiple renderers
public abstract class Shape {
protected Renderer renderer; // Bridge to implementation
}
2. Leaking Implementation Details
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Wrong - abstraction exposes implementation
public class Shape {
public Renderer getRenderer() { // Exposes implementation
return renderer;
}
}
// Right - abstraction hides implementation
public class Shape {
protected Renderer renderer; // Protected, not exposed
public void draw() {
renderer.render(this);
}
}
3. Over-Engineering Simple Cases
Don’t use Bridge when you have only one implementation:
1
2
3
4
5
6
7
8
9
10
// Overkill - only one renderer exists
public interface Renderer { }
public class TheOnlyRenderer implements Renderer { }
// Just use the class directly
public class Shape {
public void draw() {
// Direct rendering code
}
}
Real-World Examples
JDBC: DriverManager (abstraction) works with database-specific drivers (implementation).
Java AWT: Window, Panel are abstractions. Peer classes are implementations for each platform.
Logging Frameworks: SLF4J abstraction with Logback/Log4j implementations.
Remote API Clients: Client abstraction with HTTP/gRPC/WebSocket implementations.
Related Patterns
Adapter makes things work together after they exist. Bridge is planned upfront.
Abstract Factory can create implementations for the Bridge.
Strategy is similar but focuses on swapping algorithms, not separating abstraction from implementation.
Wrapping Up
Bridge separates abstraction from implementation so both can evolve independently. It prevents class explosion when you have multiple dimensions of variation.
Use it when you need to vary both what something is and how it works. The abstraction defines “what,” the implementation defines “how.”
Design Bridge upfront. If you’re retrofitting, you probably want Adapter instead.
Further Reading:
- Head First Design Patterns - Appendix
- JDBC Architecture