{} The Go Reference

Structural pattern · Gang of Four · Advanced

Bridge

Decouple an abstraction from its implementation so the two can vary independently, instead of multiplying into N×M types.

Structural Advanced ⏱ 3 min read Complete

📺 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 <|.. ASCIIRenderer

Idiomatic 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:

bridge.go — editable & runnable
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:

PatternSeparatesYou reach for it when
Bridgetwo whole hierarchies (abstraction × implementation)both dimensions evolve independently (shape × renderer)
Strategyone algorithm from its hosta single behavior should be swappable
Abstract Factorycreation of a product familyyou 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.Writer bridges writers (fmt.Fprintf, loggers) from concrete sinks (file, buffer, socket).
  • database/sql bridges the SQL API from pluggable drivers.
  • log/slog bridges the logging front-end from its Handler back-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.

Check your understanding

Score: 0 / 5

1. 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.