I. Bridge — Structural Pattern
The Bridge pattern is a structural design pattern that decouples an abstraction from its implementation so that the two can vary independently. This is the definition straight from the Gang of Four — and it's worth unpacking.
Without Bridge, combining two dimensions of variation leads to a class explosion. Imagine you have 2 computer types and 3 printer types — you'd end up with 2×3 = 6 concrete classes (MacOSCanon, MacOSEpson, WindowsCanon, ...). Add a new printer, and you need 2 more classes. Add a new computer, and you need 3 more.
Bridge solves this by splitting the two hierarchies and connecting them via composition ("has-a") instead of inheritance ("is-a"):
- Abstraction — the high-level control layer (e.g.
Computer)
- Implementation — the low-level operations layer (e.g.
Printer)
- Bridge — the abstraction holds a reference to the implementation interface
Result: M + N classes instead of M × N.
II. Real-world Example
A printing management system:
- We have two types of computers:
MacOSandWindow
- We have two types of printers:
HPandEpson
- Any computer should be able to print with any printer — without hardcoding the combination
Computerholds aPrinterreference and delegates the actual printing to it
- Swapping printers at runtime is as simple as calling
SetPrinter()

III. Implementation
printer.go — the implementation interface
package bridge
type Printer interface {
Print() error
}hp.go — concrete implementation
package bridge
import "fmt"
type HP struct{}
func (p *HP) Print() error {
fmt.Println("HP printer printing...")
return nil
}epson.go — concrete implementation
package bridge
import "fmt"
type Epson struct{}
func (p *Epson) Print() error {
fmt.Println("Epson printer printing...")
return nil
}computer.go — the abstraction interface
package bridge
type Computer interface {
Print() error
SetPrinter(printer Printer)
}macos.go — concrete abstraction
package bridge
import "fmt"
type MacOS struct {
printer Printer
}
func (m *MacOS) Print() error {
fmt.Println("MacOS printing...")
return m.printer.Print()
}
func (m *MacOS) SetPrinter(printer Printer) {
m.printer = printer
}window.go — concrete abstraction
package bridge
import "fmt"
type Window struct {
printer Printer
}
func (w *Window) Print() error {
fmt.Println("Window printing...")
return w.printer.Print()
}
func (w *Window) SetPrinter(printer Printer) {
w.printer = printer
}IV. Explain the example above
Let's walk through a concrete usage:
window := &bridge.Window{}
macOS := &bridge.MacOS{}
epson := &bridge.Epson{}
hp := &bridge.HP{}
window.SetPrinter(epson)
window.Print()
macOS.SetPrinter(hp)
macOS.Print()Output:
Window printing...
Epson printer printing...
MacOS printing...
HP printer printing...Step by step:
window.SetPrinter(epson)— the bridge is established:Windownow holds a reference toEpson
window.Print()—Windowlogs its own message, then delegates toepson.Print()
epson.Print()—Epsonlogs"Epson printer printing..."
- Same flow for
macOS+hp:MacOSlogs its message, delegates tohp.Print()
The key insight: neither Window nor MacOS knows anything about HP or Epson concretely — they only know the Printer interface. You can swap printers at runtime without touching the computer code:
// switch MacOS from HP to Epson at runtime
macOS.SetPrinter(epson)
macOS.Print()
// MacOS printing...
// Epson printer printing...Adding a new printer (e.g. Canon) only requires implementing the Printer interface — no changes to MacOS or Window. Adding a new computer (e.g. Linux) only requires implementing the Computer interface — no changes to HP or Epson.
V. Conclusion
The Bridge pattern is powerful when you have two independent dimensions of variation that would otherwise multiply into a large class hierarchy. By connecting them through composition rather than inheritance, both sides stay open for extension without affecting each other.
Trade-offs to keep in mind:
- Good fit: multiple abstraction types × multiple implementation types; need to swap implementations at runtime; want to follow the Open/Closed Principle across both hierarchies
- Poor fit: only one abstraction type or one implementation type — the indirection adds complexity without benefit; simpler problems don't warrant the pattern
No pattern is universally superior. Use Bridge when the class explosion problem is real, not just theoretical.
VI. References
- Go Design Patterns — Mario Castro Contreras




