📺 Analogy
A TV remote (the abstraction) and the TV itself (the implementation) evolve separately. A fancy universal remote works with a Sony or a Samsung; a basic remote works with both too. You don’t build a SonyFancyRemote and a SamsungBasicRemote for every pairing — the remote talks to any TV through a shared interface.
The problem
Two things vary at once: what you’re drawing (circle, square) and how it’s rendered (vector, ASCII). Model them with inheritance and you need a type for every combination — CircleVector, CircleASCII, SquareVector… an N×M explosion. Bridge separates the two hierarchies: the abstraction holds a reference to an implementation interface, so each side grows on its own.
Structure
classDiagram
class Circle {
-renderer Renderer
-radius float64
+Draw()
+Resize()
}
class Renderer {
<<interface>>
+RenderCircle(radius)
}
class VectorRenderer { +RenderCircle() }
class ASCIIRenderer { +RenderCircle() }
Circle o--> Renderer : bridges to
Renderer <|.. VectorRenderer
Renderer <|.. ASCIIRendererIdiomatic Go
In Go, Bridge is just composition: the abstraction holds the implementation as an interface field. Any shape pairs with any renderer — no combinatorial types. Edit and Run:
package main
import "fmt"
// Implementor: the "how it's rendered" dimension.
type Renderer interface {
RenderCircle(radius float64) string
}
type VectorRenderer struct{}
func (VectorRenderer) RenderCircle(r float64) string {
return fmt.Sprintf("vector circle, radius %.1f", r)
}
type ASCIIRenderer struct{}
func (ASCIIRenderer) RenderCircle(r float64) string {
return fmt.Sprintf("ascii circle, ~%d chars wide", int(r*2))
}
// Abstraction: the "what it is" dimension, bridged to a Renderer.
type Circle struct {
renderer Renderer
radius float64
}
func (c Circle) Draw() string { return c.renderer.RenderCircle(c.radius) }
func (c *Circle) Resize(f float64) { c.radius *= f }
func main() {
// any shape × any renderer — no CircleVector / CircleASCII explosion
for _, r := range []Renderer{VectorRenderer{}, ASCIIRenderer{}} {
c := Circle{renderer: r, radius: 5}
c.Resize(2)
fmt.Println(c.Draw())
}
}
🐹 Bridge is almost invisible in Go
Because Go pushes you toward “composition over inheritance,” Bridge often appears without anyone naming it — you just hold an interface field. The skill is recognizing the two independent dimensions early and refusing to let them multiply. It resembles Strategy (inject behavior) but Strategy swaps an algorithm, while Bridge separates a whole abstraction hierarchy from an implementation hierarchy.
Bridge, Strategy, or Abstract Factory?
All three lean on “program to an interface,” so they blur — separate them by what varies:
| Pattern | Separates | You reach for it when |
|---|---|---|
| Bridge | two whole hierarchies (abstraction × implementation) | both dimensions evolve independently (shape × renderer) |
| Strategy | one algorithm from its host | a single behavior should be swappable |
| Abstract Factory | creation of a product family | you need matched sets of related objects |
Bridge is the only one of the three whose job is to stop an N×M type explosion — the dead giveaway that you’ve got two axes, not one. It’s the structural embodiment of favor composition over inheritance.
In the standard library
io.Writerbridges writers (fmt.Fprintf, loggers) from concrete sinks (file, buffer, socket).database/sqlbridges the SQL API from pluggable drivers.log/slogbridges the logging front-end from itsHandlerback-ends.
Pitfalls
⚠️ Don't bridge a single dimension
If only one axis ever varies, a Bridge is over-design — a plain interface (or Strategy) says it better. The pattern earns its complexity only when both sides genuinely change independently. Introduce it when the second dimension actually appears, not before.
When to use it — and when not
✅ Reach for it when
- Two (or more) dimensions vary independently — e.g. shape × renderer, message × transport.
- You'd otherwise get a class explosion (CircleSVG, CircleCanvas, SquareSVG, …).
- You want to swap the implementation at runtime.
⛔ Think twice when
- Only one dimension actually varies — a single interface (or Strategy) is enough.
- The abstraction and implementation never change independently.
Related patterns
Convert the interface of a type into another interface clients expect, letting otherwise-incompatible types work together.
behavioralStrategyDefine a family of interchangeable algorithms, encapsulate each one, and select which to use at runtime.
creationalAbstract FactoryProvide an interface for creating families of related objects without specifying their concrete types.
Check your understanding
Score: 0 / 51. What does Bridge decouple?
Bridge splits a design along two axes and connects them with an interface, so you can combine any abstraction with any implementation without subclassing every pairing.
2. What explosion does Bridge prevent?
Without Bridge, M shapes × N renderers means M×N concrete types. With Bridge it's M + N — each side grows independently.
3. How does Bridge differ from Adapter?
Adapter is reactive — make X fit interface Y after the fact. Bridge is proactive design — keep the abstraction and implementation apart so both can evolve.
4. With M shapes and N renderers, how many types does Bridge need vs inheritance?
Inheritance forces a concrete type for every combination — CircleVector, CircleASCII, SquareVector… = M×N. Bridge keeps the two hierarchies separate and connects them with an interface field, so adding a renderer is +1, adding a shape is +1: M+N total.
5. Bridge vs Strategy — both inject an interface. What's the difference in scope?
Mechanically both hold an interface field. Intent differs: Strategy is one pluggable operation (the 'how' of one method); Bridge decouples two evolving dimensions (a refined-abstraction hierarchy on one side, an implementor hierarchy on the other). Strategy is a point; Bridge is two axes.
Comments
Sign in with GitHub to join the discussion.