Bridge Design Pattern
Decouple an abstraction from its implementation so that the two can vary independently.
Real-World Analogy
Think of a Remote Control and TV
A remote control (abstraction) works with any TV brand (implementation) — Sony, Samsung, LG. You can have a basic remote or an advanced remote (abstraction hierarchy). You can have LCD or OLED TVs (implementation hierarchy). The remote and TV vary independently — any remote works with any TV. The "bridge" is the connection between them.
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
R["📱 Any Remote"] -->|"🌉 bridge"| T["📺 Any TV"]
R --> Note1(["🎮 Basic Remote"])
R --> Note2(["🎮 Advanced Remote"])
T --> Note3{{"Sony LCD"}}
T --> Note4{{"Samsung OLED"}}
style R fill:#E3F2FD,stroke:#1565C0,stroke-width:2px,color:#000
style T fill:#E8F5E9,stroke:#2E7D32,stroke-width:2px,color:#000
style Note1 fill:#E3F2FD,stroke:#1565C0,color:#000
style Note2 fill:#E3F2FD,stroke:#1565C0,color:#000
style Note3 fill:#E8F5E9,stroke:#2E7D32,color:#000
style Note4 fill:#E8F5E9,stroke:#2E7D32,color:#000
Pattern Structure
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
Abs[["🎯 Abstraction"]] -->|"has-a (bridge)"| Impl[["⚙️ Implementor"]]
RefAbs{{"🔧 Refined Abstraction"}} -->|"extends"| Abs
ImplA{{"📦 Concrete Impl A"}} -->|"implements"| Impl
ImplB{{"📦 Concrete Impl B"}} -->|"implements"| Impl
style Abs fill:#FFF3E0,stroke:#E65100,color:#000
style RefAbs fill:#E3F2FD,stroke:#1565C0,color:#000
style Impl fill:#FFF3E0,stroke:#E65100,color:#000
style ImplA fill:#E3F2FD,stroke:#1565C0,color:#000
style ImplB fill:#E3F2FD,stroke:#1565C0,color:#000 UML Class Diagram
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
classDiagram
class Renderer {
<<interface>>
+renderCircle(radius) void
+renderSquare(side) void
+renderTriangle(base, height) void
}
class OpenGLRenderer {
+renderCircle(radius) void
+renderSquare(side) void
+renderTriangle(base, height) void
}
class SVGRenderer {
+renderCircle(radius) void
+renderSquare(side) void
+renderTriangle(base, height) void
}
class Shape {
<<abstract>>
#renderer: Renderer
+draw()* void
+resize(factor)* void
}
class Circle {
-radius: double
+draw() void
+resize(factor) void
}
class Square {
-side: double
+draw() void
+resize(factor) void
}
OpenGLRenderer ..|> Renderer : implements
SVGRenderer ..|> Renderer : implements
Circle --|> Shape : extends
Square --|> Shape : extends
Shape o-- Renderer : bridge Without Bridge (Class Explosion)
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
Shape{{"💥 Shape"}} --> RC(["Red Circle"])
RC --> BC(["Blue Circle"])
BC --> RS(["Red Square"])
RS --> BS(["Blue Square"])
BS --> RR(["Red Rectangle"])
RR --> BR(["Blue Rectangle"])
style Shape fill:#FFF3E0,stroke:#E65100,stroke-width:2px,color:#000
style RC fill:#FCE4EC,stroke:#C62828,color:#000
style BC fill:#E3F2FD,stroke:#1565C0,color:#000
style RS fill:#FCE4EC,stroke:#C62828,color:#000
style BS fill:#E3F2FD,stroke:#1565C0,color:#000
style RR fill:#FCE4EC,stroke:#C62828,color:#000
style BR fill:#E3F2FD,stroke:#1565C0,color:#000 6 classes for 3 shapes x 2 colors. Adding one color = 3 more classes. Adding one shape = 2 more classes. This grows as M x N!
The Problem
You have a Shape class that can be rendered in different ways (OpenGL, DirectX, SVG). You also want to have different shapes (Circle, Square, Triangle). Without Bridge:
CircleOpenGL,CircleDirectX,CircleSVGSquareOpenGL,SquareDirectX,SquareSVGTriangleOpenGL,TriangleDirectX,TriangleSVG
That's M shapes x N renderers = M*N classes. Adding a new renderer means modifying every shape. This violates the Open/Closed Principle and creates maintenance nightmares.
Without This Pattern
// BAD: Separate class for EVERY combination of shape + renderer
public class CircleOpenGL {
public void draw() { System.out.println("OpenGL circle"); }
}
public class CircleDirectX {
public void draw() { System.out.println("DirectX circle"); }
}
public class CircleSVG {
public void draw() { System.out.println("SVG circle"); }
}
public class SquareOpenGL {
public void draw() { System.out.println("OpenGL square"); }
}
public class SquareDirectX {
public void draw() { System.out.println("DirectX square"); }
}
public class SquareSVG {
public void draw() { System.out.println("SVG square"); }
}
// Adding TriangleOpenGL, TriangleDirectX, TriangleSVG...
// Adding a new renderer (Vulkan)? 3 MORE classes!
// Adding a new shape (Hexagon)? 3 MORE classes!
Problems:
- Class explosion (M x N): 3 shapes x 3 renderers = 9 classes; 10 x 10 = 100 classes — growth is multiplicative, not additive
- Violates Open/Closed Principle: Adding one new renderer forces you to create a new subclass for EVERY existing shape
- Massive code duplication: Each
CircleOpenGL,CircleDirectX, etc. duplicates the circle-drawing logic with minor rendering variations - Impossible to switch renderer at runtime: The renderer is baked into the class hierarchy — you cannot swap OpenGL for SVG without creating a new object of a completely different type
- Pain point: Your codebase grows out of control the moment a stakeholder says "we also need to support Vulkan rendering" — suddenly you must create N new classes, test them all, and maintain them forever
The Solution
Bridge splits the monolithic hierarchy into two independent hierarchies:
- Abstraction — the high-level control layer (e.g., Shape)
- Implementation — the low-level platform layer (e.g., Renderer)
Connected by a bridge (composition). Now you have M + N classes instead of M * N.
The Math
- Without Bridge: 3 shapes x 3 renderers = 9 classes
- With Bridge: 3 shapes + 3 renderers = 6 classes
- At scale (10 x 10): Without = 100, With = 20
Implementation
// Implementation interface (the "bridge" target)
public interface Renderer {
void renderCircle(double radius);
void renderSquare(double side);
void renderTriangle(double base, double height);
}
// Concrete Implementations
public class OpenGLRenderer implements Renderer {
@Override
public void renderCircle(double radius) {
System.out.println("OpenGL: Drawing circle with radius " + radius);
}
@Override
public void renderSquare(double side) {
System.out.println("OpenGL: Drawing square with side " + side);
}
@Override
public void renderTriangle(double base, double height) {
System.out.println("OpenGL: Drawing triangle " + base + "x" + height);
}
}
public class SVGRenderer implements Renderer {
@Override
public void renderCircle(double radius) {
System.out.println("SVG: <circle r=\"" + radius + "\"/>");
}
@Override
public void renderSquare(double side) {
System.out.println("SVG: <rect width=\"" + side + "\" height=\"" + side + "\"/>");
}
@Override
public void renderTriangle(double base, double height) {
System.out.println("SVG: <polygon points=\"...\"/> (triangle)");
}
}
// Abstraction
public abstract class Shape {
protected final Renderer renderer; // BRIDGE
protected 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 radius;
public Circle(Renderer renderer, double radius) {
super(renderer);
this.radius = radius;
}
@Override
public void draw() {
renderer.renderCircle(radius);
}
@Override
public void resize(double factor) {
radius *= factor;
}
}
public class Square extends Shape {
private double side;
public Square(Renderer renderer, double side) {
super(renderer);
this.side = side;
}
@Override
public void draw() {
renderer.renderSquare(side);
}
@Override
public void resize(double factor) {
side *= factor;
}
}
// Client — mix any shape with any renderer
public class DrawingApp {
public static void main(String[] args) {
Renderer opengl = new OpenGLRenderer();
Renderer svg = new SVGRenderer();
Shape circle = new Circle(opengl, 5.0);
Shape square = new Square(svg, 10.0);
circle.draw(); // OpenGL: Drawing circle with radius 5.0
square.draw(); // SVG: <rect width="10.0" height="10.0"/>
// Easy to switch renderer at runtime!
Shape svgCircle = new Circle(svg, 3.0);
svgCircle.draw(); // SVG: <circle r="3.0"/>
}
}
// Implementation — how to send
public interface MessageSender {
void send(String recipient, String message);
}
public class EmailSender implements MessageSender {
@Override
public void send(String recipient, String message) {
System.out.println("Email to " + recipient + ": " + message);
}
}
public class SmsSender implements MessageSender {
@Override
public void send(String recipient, String message) {
System.out.println("SMS to " + recipient + ": " + message);
}
}
public class SlackSender implements MessageSender {
@Override
public void send(String recipient, String message) {
System.out.println("Slack to #" + recipient + ": " + message);
}
}
// Abstraction — what to send
public abstract class Notification {
protected final MessageSender sender; // BRIDGE
protected Notification(MessageSender sender) {
this.sender = sender;
}
public abstract void notify(String recipient, String event);
}
// Refined Abstractions
public class UrgentNotification extends Notification {
public UrgentNotification(MessageSender sender) {
super(sender);
}
@Override
public void notify(String recipient, String event) {
String message = "🚨 URGENT: " + event + " — Immediate action required!";
sender.send(recipient, message);
sender.send(recipient, "REMINDER: " + message); // Send twice for urgency
}
}
public class RegularNotification extends Notification {
public RegularNotification(MessageSender sender) {
super(sender);
}
@Override
public void notify(String recipient, String event) {
sender.send(recipient, "Info: " + event);
}
}
// Usage — any notification type x any sender
public class AlertSystem {
public static void main(String[] args) {
Notification urgentEmail = new UrgentNotification(new EmailSender());
Notification regularSlack = new RegularNotification(new SlackSender());
Notification urgentSms = new UrgentNotification(new SmsSender());
urgentEmail.notify("admin@company.com", "Server CPU at 95%");
regularSlack.notify("engineering", "Deploy v2.3 complete");
urgentSms.notify("+1234567890", "Database connection pool exhausted");
}
}
// Implementation — database technology
public interface DatabaseEngine {
void connect(String connectionString);
void execute(String query);
List<Map<String, Object>> fetch(String query);
void disconnect();
}
public class MySqlEngine implements DatabaseEngine {
@Override
public void connect(String connectionString) {
System.out.println("MySQL connected: " + connectionString);
}
@Override
public void execute(String query) {
System.out.println("MySQL executing: " + query);
}
@Override
public List<Map<String, Object>> fetch(String query) {
System.out.println("MySQL fetching: " + query);
return List.of();
}
@Override
public void disconnect() {
System.out.println("MySQL disconnected");
}
}
public class PostgresEngine implements DatabaseEngine {
@Override
public void connect(String connectionString) {
System.out.println("Postgres connected: " + connectionString);
}
@Override
public void execute(String query) {
System.out.println("Postgres executing: " + query);
}
@Override
public List<Map<String, Object>> fetch(String query) {
System.out.println("Postgres fetching: " + query);
return List.of();
}
@Override
public void disconnect() {
System.out.println("Postgres disconnected");
}
}
// Abstraction — repository operations
public abstract class Repository<T> {
protected final DatabaseEngine engine;
protected Repository(DatabaseEngine engine) {
this.engine = engine;
}
public abstract void save(T entity);
public abstract T findById(Long id);
public abstract List<T> findAll();
}
// Refined Abstraction
public class UserRepository extends Repository<User> {
public UserRepository(DatabaseEngine engine) {
super(engine);
}
@Override
public void save(User user) {
engine.execute("INSERT INTO users (name, email) VALUES ('" +
user.name() + "', '" + user.email() + "')");
}
@Override
public User findById(Long id) {
List<Map<String, Object>> results = engine.fetch(
"SELECT * FROM users WHERE id = " + id);
// Map results to User
return new User(id, "mapped_name", "mapped_email");
}
@Override
public List<User> findAll() {
engine.fetch("SELECT * FROM users");
return List.of();
}
}
// Swap database without touching repository logic!
UserRepository mysqlRepo = new UserRepository(new MySqlEngine());
UserRepository pgRepo = new UserRepository(new PostgresEngine());
When to Use
- You want to avoid a permanent binding between abstraction and implementation
- Both abstractions and implementations should be extensible independently via subclassing
- You have a Cartesian product problem (M x N class explosion)
- You need to switch implementations at runtime
- You want to share an implementation among multiple abstractions
- You're building platform-independent code (e.g., same logic, different OS/DB/renderer)
Real-World Examples
| Where | Example |
|---|---|
| JDBC | DriverManager (abstraction) + vendor drivers (implementation) — classic Bridge |
| JDK | java.util.logging.Handler + Formatter |
| Spring | AbstractPlatformTransactionManager + vendor transaction implementations |
| SLF4J | Logging API (abstraction) bridged to Logback/Log4j (implementation) |
| Hibernate | Dialect — same ORM, different SQL dialects per database |
| AWT | java.awt.peer — platform-independent widgets bridged to OS-specific rendering |
| Android | View system bridges UI logic to platform-specific drawing |
Pitfalls
Common Mistakes
- Over-engineering: Don't use Bridge when you only have one dimension of variation — YAGNI
- Confusing with Strategy: Bridge is structural (splits class hierarchy); Strategy is behavioral (swaps algorithms). Bridge is designed upfront; Strategy is often added later
- Leaky abstractions: The abstraction shouldn't expose implementation details — keep the bridge clean
- Too many dimensions: If you have 3+ dimensions, consider combining Bridge with other patterns
- Forgetting runtime swapping: If you never swap implementations at runtime, you might just need an interface (simpler than full Bridge)
Key Takeaways
Summary
| Aspect | Detail |
|---|---|
| Intent | Separate "what" (abstraction) from "how" (implementation) |
| Mechanism | Composition — abstraction holds reference to implementor |
| Key Benefit | Eliminates M x N class explosion; both sides extend independently |
| Key Principle | Prefer composition over inheritance |
| vs Strategy | Bridge separates hierarchies at design time; Strategy swaps behavior at runtime |
| Interview Tip | "JDBC is the classic Bridge — your code uses java.sql interfaces while vendor drivers provide the implementation" |