Bridge — Optimize¶
Each section presents a Bridge that works but is wasteful. Profile, optimize, measure.
Table of Contents¶
- Optimization 1: Stateless implementors share one instance
- Optimization 2: Group by type to avoid megamorphism (Java)
- Optimization 3: Batch the implementor call
- Optimization 4: Memoize values across the bridge
- Optimization 5: Pointer receivers + reused interface (Go)
- Optimization 6: Decorator stack on implementor
- Optimization 7: Lazy-construct heavy implementors
- Optimization 8: Drop the Bridge when only one implementor exists
- Optimization 9: Specialize hot-path combinations
- Optimization 10: Data-oriented Bridge for large arrays
- Optimization Tips
Optimization 1: Stateless implementors share one instance¶
Before¶
for (User u : users) {
Logger logger = new Logger(new ConsoleSink()); // new ConsoleSink per user!
logger.info("processing " + u.id());
process(u);
}
Cost: Every loop iteration allocates a fresh ConsoleSink (and Logger). Even if the sink is stateless, allocation costs and GC pressure add up. With 1M users, a million pointless allocations.
After¶
private static final Sink SHARED_SINK = new ConsoleSink();
private static final Logger SHARED_LOGGER = new Logger(SHARED_SINK);
for (User u : users) {
SHARED_LOGGER.info("processing " + u.id());
process(u);
}
Measurement. GC time drops; allocation profile flattens.
Lesson: Stateless implementors are reusable. Construct once, share.
Optimization 2: Group by type to avoid megamorphism (Java)¶
Before¶
// 8 shape types, 4 renderer types, fully shuffled.
List<Shape> all = ...;
for (Shape s : all) s.draw();
Cost: Both the Shape.draw() site and the Renderer.render* site go megamorphic. JIT inline caches fail; every call is a vtable lookup.
After¶
Map<Class<?>, List<Shape>> byClass = all.stream()
.collect(Collectors.groupingBy(Shape::getClass));
byClass.forEach((cls, group) -> {
for (Shape s : group) s.draw(); // monomorphic at this site
});
Measurement. JMH: 5-8 ns/call → 1-2 ns/call after grouping. Throughput up ~3-4×.
Lesson: When both sides of a Bridge are polymorphic, group iteration to keep call sites monomorphic.
Optimization 3: Batch the implementor call¶
Before¶
public void render(List<Shape> shapes) {
for (Shape s : shapes) renderer.render(s); // 1 call per shape
}
After (when the implementor supports batches)¶
Cost. Per-call overhead, GPU command-buffer flushes, network round-trips — all multiplied by N before, paid once after.
Measurement. A batched OpenGL renderer drops a 4 ms per-frame loop to 0.4 ms. Database batch insert: 100× faster.
Lesson: If the implementor supports batching, expose batched methods on the abstraction. Don't hide it behind a per-item interface.
Optimization 4: Memoize values across the bridge¶
Before¶
public class Circle extends Shape {
public Geometry geometry() { return computeTessellation(); } // expensive
}
public class VectorRenderer implements Renderer {
public void renderShape(Shape s) {
Geometry g = s.geometry(); // call 1
if (looksWeird(g)) g = s.geometry(); // call 2 — recomputes!
...
}
}
Cost: Tessellation can run twice. Profiler shows 60% of time in geometry computation.
After¶
public abstract class Shape {
private Geometry cached;
public final Geometry geometry() {
if (cached == null) cached = computeGeometry();
return cached;
}
protected abstract Geometry computeGeometry();
}
Measurement. Frame time halves on geometry-heavy scenes. Memory cost: one extra reference per shape.
Lesson: When the abstraction has expensive computed values that the implementor calls multiple times, memoize on the abstraction.
Optimization 5: Pointer receivers + reused interface (Go)¶
Before¶
type Renderer interface { Render(s Shape) }
type RasterRenderer struct{ /* ... */ }
func (r RasterRenderer) Render(s Shape) { ... } // value receiver
for _, s := range shapes {
var r Renderer = RasterRenderer{...} // allocates per iter!
r.Render(s)
}
Cost: Each var r Renderer = RasterRenderer{} allocates a copy on the heap (interface needs a stable pointer). Per iteration → GC churn.
After¶
func (r *RasterRenderer) Render(s Shape) { ... } // pointer receiver
renderer := &RasterRenderer{...}
var r Renderer = renderer
for _, s := range shapes {
r.Render(s)
}
Measurement. pprof -alloc_objects: zero allocations in the loop. CPU drops slightly; GC pauses drop more.
Lesson: In Go, Bridge implementors should use pointer receivers and be passed as pointers through interfaces. Construct once.
Optimization 6: Decorator stack on implementor¶
Before¶
The abstraction does retries and metrics inline:
public abstract class Notification {
protected final Channel ch;
public void send(String to, String body) {
long start = System.nanoTime();
for (int attempt = 0; attempt < 3; attempt++) {
try { ch.send(to, body); break; }
catch (TransientException e) { /* sleep */ }
}
metrics.record(System.nanoTime() - start);
}
}
After¶
Move retries and metrics into Decorators on the implementor:
public class RetryingChannel implements Channel {
private final Channel inner; private final int attempts;
public void send(String to, String body) {
for (int i = 0; i < attempts; i++) {
try { inner.send(to, body); return; }
catch (TransientException e) { if (i == attempts - 1) throw e; }
}
}
}
public class MeteredChannel implements Channel {
private final Channel inner; private final Metrics m;
public void send(String to, String body) {
long start = System.nanoTime();
try { inner.send(to, body); }
finally { m.record(System.nanoTime() - start); }
}
}
// Wiring
Channel ch = new MeteredChannel(new RetryingChannel(new EmailChannel(client), 3), metrics);
Notification n = new Welcome(ch);
Measurement. Notification's logic shrinks; decorators are independently testable; new abstractions get retries/metrics for free.
Lesson: Cross-cutting concerns belong in decorators on the implementor side, not in the abstraction.
Optimization 7: Lazy-construct heavy implementors¶
Before¶
public class Logger {
private final Sink fileSink = new FileSink("/var/log/app.log"); // opens file at load
private final Sink netSink = new NetworkSink("logs.example.com:514"); // opens socket
}
Cost: Every Logger constructed opens a file and a socket — even if you only ever use the console. Boot time and resource usage explode.
After¶
public class Logger {
private final Supplier<Sink> sinkSupplier;
private Sink resolved;
public Logger(Supplier<Sink> s) { this.sinkSupplier = s; }
private Sink sink() {
if (resolved == null) resolved = sinkSupplier.get();
return resolved;
}
public void info(String msg) { sink().emit("INFO", msg); }
}
Or use a DI container with lazy beans.
Measurement. Boot time drops from 10s to 1s on test runs that only use console logging.
Lesson: Implementors with heavy initialization should be lazy. Construct on first use.
Optimization 8: Drop the Bridge when only one implementor exists¶
Before¶
public interface Storage { void save(...); byte[] load(...); }
public final class FileStorage implements Storage { ... } // only implementor
public class Repo {
private final Storage s;
public Repo(Storage s) { this.s = s; }
}
After 18 months, still no second implementor.
After¶
public final class FileStorage { void save(...); byte[] load(...); }
public class Repo {
private final FileStorage s;
public Repo(FileStorage s) { this.s = s; }
}
Measurement. Code size drops, IDE navigation faster, dispatch slightly faster (CHA more aggressive).
Caveat: if the interface enables fast tests via a fake, that fake counts as a second implementor — keep it.
Lesson: Don't keep abstractions you never benefit from. Reverse over-engineering aggressively.
Optimization 9: Specialize hot-path combinations¶
Before¶
Generic Bridge handles all combinations through dispatch:
After¶
For the single hottest combination (e.g., 95% are Sprite × VectorRenderer), specialize:
if (renderer instanceof VectorRenderer && allSprites(allItems)) {
VectorRenderer vr = (VectorRenderer) renderer;
for (Sprite s : allItems) vr.renderSprite(s); // monomorphic, inlinable
} else {
for (Drawable d : allItems) renderer.render(d); // generic path
}
Cost. A small instanceof check; one specialized loop alongside the generic one.
Measurement. Hot-path FPS doubles in profiler-driven test scenes.
Lesson: Profile-guided specialization is appropriate when one combination dominates. Don't preemptively unroll all combinations.
Optimization 10: Data-oriented Bridge for large arrays¶
Before¶
Array of pointers to Bridges:
sprites := make([]Sprite, 1_000_000) // each is a pointer to an obj
for i := range sprites { sprites[i].Draw() } // dereferences scattered memory
Cost: Cache misses dominate; each sprite + its renderer + the renderer's vtable live in scattered cache lines. Per-frame budget blown.
After¶
Struct of arrays (data-oriented design):
type Sprites struct {
Xs, Ys, Sizes []float32
Renderer Renderer
}
func (sp *Sprites) Draw() {
sp.Renderer.RenderBatch(sp.Xs, sp.Ys, sp.Sizes) // one call, contiguous memory
}
Cost: Architectural shift; not always feasible.
Measurement. Game engines see 2-5× FPS improvements on heavy sprite scenes.
Lesson: Bridge with array-of-objects is fine for thousands; for millions, consider struct-of-arrays + batched implementor calls.
Optimization Tips¶
- Profile before optimizing. "Bridge is slow" is rarely the actual cause. Most slowness is in the implementor's I/O.
- Stateless implementors are free to share. Avoid per-call allocations.
- Watch for megamorphism. Two-axis polymorphism at the same site degrades JIT optimization. Group by type.
- Batch when the implementor supports it. Per-item interfaces hide batching power.
- Memoize across the bridge. Computations called multiple times by the implementor should be cached on the abstraction.
- Use pointer receivers in Go. Avoid per-iteration interface allocation traps.
- Move cross-cutting concerns into decorators on the implementor. Keep the abstraction focused.
- Lazy-construct heavy implementors. Don't pay for what's not used.
- Drop the Bridge if it never paid off. Reverse over-engineering aggressively.
- Specialize the hot path. When 95% of calls are one combination, write a fast loop for it; keep the generic path for the rest.
- Don't optimize what the JIT erases. Microbench first; HotSpot often makes Bridge dispatch free.
- Optimize for change too. A clean Bridge that's easy to swap is more valuable than a tweaked one nobody understands.
← Back to Bridge folder · ↑ Structural Patterns · ↑↑ Roadmap Home
You've completed the Bridge pattern suite. Continue to: Composite · Decorator · Facade