Opening low-level design
Progress is saved as you study.
Opening low-level design
Progress is saved as you study.
Opening your learning path
Progress is saved as you study.
Bridge, Flyweight & Composite Deep Dive is a depth lesson in the Strategy & Factory Patterns family of the Thita low level design course.
#1 and #4 most tested patterns: eliminate if/else chains with Strategy and create objects without specifying exact classes with Factory.
It sits on the Deep Dive path — worth reading once the Interview Soon lessons are solid, or when a role names this area explicitly.
Bridge handles class explosion. Flyweight handles memory growth from repeated object state. Composite handles recursive tree behavior.
Use Bridge when two dimensions grow independently, such as Shape × Renderer. Use Flyweight when many objects repeat the same heavy data, such as tiles sharing textures. Use Composite when objects form a tree, such as products inside nested bundles.
Bridge fits when two dimensions can grow separately. A common example is Shape and Renderer: shapes can be Circle or Square, and renderers can be OpenGL or DirectX.
Without Bridge, the class list grows like this:
OpenGLCircleDirectXCircleOpenGLSquareDirectXSquareA 2 × 2 design has 4 classes. A 6 × 5 design has 30 classes, and most classes repeat the same logic.
Think of Bridge when the problem has:
“M business abstractions and N platform implementations.”
The signal is M × N class growth. Bridge replaces inheritance multiplication with composition.
Each shape class knows about a renderer. Adding one renderer forces one new class per shape. Adding one shape forces one new class per renderer.
Shape holds a Renderer. A new renderer adds one Renderer implementation. Existing shapes stay the same.
A new shape adds one Shape subclass. If the renderer interface already has the drawing operations the shape needs, existing renderers stay the same. If the new shape needs a new renderer operation, each renderer must implement that operation.
Combining every shape with every renderer needs M x N classes; Bridge keeps shapes and renderers separate and joins them with one reference, needing M + N.
| Pattern | Problem It Solves | Timing | Example |
|---|---|---|---|
| Bridge | Prevents M × N class growth | Designed before the hierarchy grows | Shape uses Renderer |
| Adapter | Connects incompatible existing APIs | Added after an API mismatch exists | OldPrinter adapted to Printer |
| Strategy | Swaps behavior within one dimension | Designed for algorithm choice | PaymentStrategy |
Bridge separates two dimensions that you expect to grow. Adapter wraps an existing API so it matches the interface your code expects.
Skip Bridge when only one side varies. If you have Circle, Square, and Triangle, and rendering never changes, plain inheritance or simple composition is enough.
Skip Bridge when the abstraction makes the code harder to read. Interviewers penalize extra layers that solve no current growth problem.
1interface Renderer {
2 void renderCircle(double radius);
3}
4
5class OpenGLRenderer implements Renderer {
6 @Override
7 public void renderCircle(double radius) {
8 System.out.println("OpenGL circle radius " + radius);
9 }
10}
11
12class DirectXRenderer implements Renderer {
13 @Override
14 public void renderCircle(double radius) {
15 System.out.println("DirectX circle radius " + radius);
16 }
17}
18
19abstract class Shape {
20 protected final Renderer renderer;
21
22 protected Shape(Renderer renderer) {
23 this.renderer = renderer;
24 }
25
26 public abstract void draw();
27}
28
29class Circle extends Shape {
30 private final double radius;
31
32 public Circle(Renderer renderer, double radius) {
33 super(renderer);
34 this.radius = radius;
35 }
36
37 @Override
38 public void draw() {
39 renderer.renderCircle(radius);
40 }
41}
42
43public class Main {
44 public static void main(String[] args) {
45 Shape shape = new Circle(new OpenGLRenderer(), 5.0);
46 shape.draw();
47 }
48}Flyweight fits when many objects repeat the same heavy data. Flyweight lowers memory usage by storing shared data once.
A game map is a clear example. If 1 million tiles each store a 1KB texture, the textures use about 1GB. If the map has only 50 tile types, Flyweight stores 50 shared textures. Each tile stores only its position and a reference to the shared type.
| State Type | Meaning | Tile Example |
|---|---|---|
| Intrinsic state | Shared, immutable, reused | Texture, terrain type |
| Extrinsic state | Unique per object or call | x, y, damage |
| Factory cache | Stores and returns shared objects | TileTypeFactory |
Intrinsic state belongs inside the flyweight object. Extrinsic state is passed into methods or stored in small context objects.
Use simple math. Do not claim Flyweight helps unless the memory drop is clear.
Without Flyweight:
11,000,000 tiles × 1KB texture = ~1GBWith Flyweight:
150 tile types × 1KB texture = ~50KB
21,000,000 tile positions × 16B = ~16MB
3Total ≈ 16MBThe cache creates the saving. If the code still creates a new texture per tile, the design does not use Flyweight correctly.
Skip Flyweight for a few hundred small objects. The factory, cache, and state split make the code harder to read.
Skip Flyweight when the shared state must be mutable. Shared mutable state causes one object’s change to affect other objects that use the same flyweight.
1import java.util.*;
2
3final class TileType {
4 private final String name;
5 private final String texture;
6
7 public TileType(String name, String texture) {
8 this.name = name;
9 this.texture = texture;
10 }
11
12 public void draw(int x, int y) {
13 System.out.println(name + " at " + x + "," + y + " using " + texture);
14 }
15}
16
17class TileTypeFactory {
18 private final Map<String, TileType> cache = new HashMap<>();
19
20 public TileType get(String name, String texture) {
21 String key = name + ":" + texture;
22 return cache.computeIfAbsent(key, k -> new TileType(name, texture));
23 }
24}
25
26class Tile {
27 private final int x;
28 private final int y;
29 private final TileType type;
30
31 public Tile(int x, int y, TileType type) {
32 this.x = x;
33 this.y = y;
34 this.type = type;
35 }
36
37 public void draw() {
38 type.draw(x, y);
39 }
40}
41
42public class Main {
43 public static void main(String[] args) {
44 TileTypeFactory factory = new TileTypeFactory();
45 Tile tile = new Tile(10, 20, factory.get("grass", "grass.png"));
46 tile.draw();
47 }
48}Composite fits when the domain forms a tree and the same operation must work on single nodes and groups.
A shopping cart with nested bundles is a common example. A product has a price. A bundle contains products or other bundles. Both support price().
The caller asks for price() without checking whether the object is a product or a bundle.
Think Composite when the problem says:
“A group can contain items, and those items can also be groups.”
A flat list loses the parent-child structure. Composite keeps the structure inside the model.
A leaf does the work directly. A composite asks its children for results and combines those results.
A flat list works when grouping has no behavior. Composite fits when grouping affects traversal, pricing, permissions, rendering, or deletion.
| Requirement | Flat List | Composite |
|---|---|---|
| Sum all prices | Works | Works |
| Nested bundles | Awkward | Natural |
| Recursive operation | Caller handles recursion | Object model handles recursion |
| Same call for item and group | Type checks needed | Same interface works |
Composite usually uses depth-first traversal. Each composite asks its children for results.
Common operations:
price() returns the sum of child prices.render() renders each child.delete() deletes children before deleting the folder.count() returns 1 + sum(child.count()) when the composite itself should be counted.Skip Composite when the structure is always one level deep. A plain list is simpler and easier to test.
Keep child-management methods on composite classes when leaves cannot support them. For example, put add() on Bundle, because Product cannot add children.
1import java.util.*;
2
3interface CartItem {
4 double price();
5}
6
7class Product implements CartItem {
8 private final String name;
9 private final double price;
10
11 public Product(String name, double price) {
12 this.name = name;
13 this.price = price;
14 }
15
16 @Override
17 public double price() {
18 return price;
19 }
20}
21
22class Bundle implements CartItem {
23 private final String name;
24 private final List<CartItem> items = new ArrayList<>();
25
26 public Bundle(String name) {
27 this.name = name;
28 }
29
30 public void add(CartItem item) {
31 items.add(item);
32 }
33
34 @Override
35 public double price() {
36 return items.stream().mapToDouble(CartItem::price).sum();
37 }
38}
39
40public class Main {
41 public static void main(String[] args) {
42 Bundle gift = new Bundle("gift");
43 gift.add(new Product("book", 20));
44 gift.add(new Product("pen", 5));
45 System.out.println(gift.price());
46 }
47}Use the pattern that matches the growth pressure.
| Pattern | Main Pressure | Main Tool | Interview Line |
|---|---|---|---|
| Bridge | M × N class explosion | Composition between abstraction and implementation | “Both axes vary independently.” |
| Flyweight | Duplicate heavy state across many objects | Shared immutable intrinsic state | “Memory drops because shared state is cached.” |
| Composite | Recursive object structure | Common interface for leaf and group | “The caller uses the same method for item and group.” |
Use Bridge for two independent dimensions, such as Shape × Renderer.
Use Flyweight when shared immutable state creates a clear memory saving. Show the memory math.
Use Composite when objects form a tree and the same operation must work on leaves and groups.
State the design pressure first. Then name the pattern.
OpenGLCircle, DirectXCircle, and OpenGLSquare, then calling the design extensible. Those classes are the class explosion Bridge avoids.instanceof or type checks.add() on the shared CartItem interface when leaves cannot support it. Keep child-management methods on composite classes.Use Bridge for platform or UI variation.
Use Flyweight for memory-heavy object pools, tile maps, text rendering, and string interning.
Use Composite for carts, file systems, menus, and organization trees.