You’re building a text editor. A document has a million characters. Each character has a font, size, color, and position. Storing all that for every character would use gigabytes of memory.

Most characters share the same formatting. Flyweight lets you share that common state.

What is the Flyweight Pattern?

Flyweight minimizes memory by sharing common state between multiple objects. Instead of each object storing all its data, objects share intrinsic (constant) state and receive extrinsic (varying) state from outside.

Flyweight Design Pattern class diagram showing Factory caching shared Flyweight objects with intrinsic state while Client provides extrinsic state

The factory ensures flyweights are shared. Clients store only extrinsic state and reference shared flyweights.

When to Use Flyweight

Use Flyweight When Skip Flyweight When
You have millions of similar objects Few objects exist
Memory usage is a concern Memory is plentiful
Most state can be shared Objects are mostly unique
Object identity isn’t important Each object must be distinct

Flyweight trades computation (looking up shared objects) for memory savings.

Implementation

Text Editor Character Formatting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// Flyweight - shared formatting data
public class CharacterFormat {
    private final String fontFamily;
    private final int fontSize;
    private final String color;
    private final boolean bold;
    private final boolean italic;
    
    public CharacterFormat(String fontFamily, int fontSize, String color, 
                           boolean bold, boolean italic) {
        this.fontFamily = fontFamily;
        this.fontSize = fontSize;
        this.color = color;
        this.bold = bold;
        this.italic = italic;
    }
    
    // Render character with extrinsic state (position, actual character)
    public void render(char character, int x, int y) {
        System.out.printf("Rendering '%c' at (%d,%d) with %s %dpx %s%s%s%n",
            character, x, y, fontFamily, fontSize, color,
            bold ? " bold" : "", italic ? " italic" : "");
    }
    
    // Equals and hashCode for sharing
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        CharacterFormat that = (CharacterFormat) o;
        return fontSize == that.fontSize && bold == that.bold && italic == that.italic
            && fontFamily.equals(that.fontFamily) && color.equals(that.color);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(fontFamily, fontSize, color, bold, italic);
    }
}

// Flyweight Factory
public class CharacterFormatFactory {
    private static final Map<CharacterFormat, CharacterFormat> formats = new HashMap<>();
    
    public static CharacterFormat getFormat(String fontFamily, int fontSize, 
                                             String color, boolean bold, boolean italic) {
        CharacterFormat key = new CharacterFormat(fontFamily, fontSize, color, bold, italic);
        return formats.computeIfAbsent(key, k -> k);
    }
    
    public static int getFormatCount() {
        return formats.size();
    }
}

// Client - stores extrinsic state
public class Character {
    private final char character;
    private final int x;
    private final int y;
    private final CharacterFormat format;  // Shared flyweight
    
    public Character(char character, int x, int y, CharacterFormat format) {
        this.character = character;
        this.x = x;
        this.y = y;
        this.format = format;
    }
    
    public void render() {
        format.render(character, x, y);
    }
}

Usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Document with many characters, few formats
List<Character> document = new ArrayList<>();

// Most text uses default format (shared)
CharacterFormat defaultFormat = CharacterFormatFactory.getFormat(
    "Arial", 12, "black", false, false
);

// Title uses different format (also shared among title characters)
CharacterFormat titleFormat = CharacterFormatFactory.getFormat(
    "Arial", 24, "blue", true, false
);

// Add title characters
String title = "Document Title";
int x = 0;
for (char c : title.toCharArray()) {
    document.add(new Character(c, x, 0, titleFormat));
    x += 20;
}

// Add body text - millions of characters, same format
String bodyText = "Lorem ipsum dolor sit amet... (imagine millions of characters)";
x = 0;
int y = 50;
for (char c : bodyText.toCharArray()) {
    document.add(new Character(c, x, y, defaultFormat));
    x += 10;
    if (x > 800) {
        x = 0;
        y += 20;
    }
}

System.out.println("Total characters: " + document.size());
System.out.println("Unique formats: " + CharacterFormatFactory.getFormatCount());
// Millions of characters but only 2 format objects

Game Particles Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// Flyweight - shared particle data
public class ParticleType {
    private final String texturePath;
    private final byte[] textureData;  // Large - several KB
    private final String color;
    private final double size;
    
    public ParticleType(String texturePath, String color, double size) {
        this.texturePath = texturePath;
        this.textureData = loadTexture(texturePath);  // Expensive
        this.color = color;
        this.size = size;
    }
    
    private byte[] loadTexture(String path) {
        // Load texture from disk - simulated
        return new byte[10000];  // 10KB texture
    }
    
    public void render(double x, double y, double velocity, double angle) {
        // Render particle at position with its properties
        System.out.printf("Particle at (%.1f, %.1f) velocity=%.1f angle=%.1f%n",
            x, y, velocity, angle);
    }
}

// Flyweight Factory
public class ParticleTypeFactory {
    private static final Map<String, ParticleType> types = new HashMap<>();
    
    public static ParticleType getType(String name) {
        return types.get(name);
    }
    
    public static void registerType(String name, ParticleType type) {
        types.put(name, type);
    }
    
    static {
        // Pre-register common particle types
        registerType("bullet", new ParticleType("bullet.png", "yellow", 2.0));
        registerType("smoke", new ParticleType("smoke.png", "gray", 10.0));
        registerType("spark", new ParticleType("spark.png", "orange", 1.0));
        registerType("explosion", new ParticleType("explosion.png", "red", 50.0));
    }
}

// Client - individual particle with extrinsic state
public class Particle {
    private double x;
    private double y;
    private double velocityX;
    private double velocityY;
    private double angle;
    private final ParticleType type;  // Shared
    
    public Particle(double x, double y, double vx, double vy, ParticleType type) {
        this.x = x;
        this.y = y;
        this.velocityX = vx;
        this.velocityY = vy;
        this.type = type;
    }
    
    public void update(double deltaTime) {
        x += velocityX * deltaTime;
        y += velocityY * deltaTime;
        angle = Math.atan2(velocityY, velocityX);
    }
    
    public void render() {
        double velocity = Math.sqrt(velocityX * velocityX + velocityY * velocityY);
        type.render(x, y, velocity, angle);
    }
}

// Usage - spawn thousands of particles
List<Particle> particles = new ArrayList<>();
ParticleType bulletType = ParticleTypeFactory.getType("bullet");

for (int i = 0; i < 10000; i++) {
    particles.add(new Particle(
        Math.random() * 1000,
        Math.random() * 1000,
        Math.random() * 10 - 5,
        Math.random() * 10 - 5,
        bulletType  // All share same type object
    ));
}

// 10,000 particles but only 1 bullet texture loaded
System.out.println("Particles: " + particles.size());
System.out.println("Memory saved: ~100MB (10KB texture x 10,000)");

String Interning as Flyweight

Java’s String pool is a built-in Flyweight:

1
2
3
4
5
6
7
8
9
10
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello").intern();

System.out.println(s1 == s2);  // true - same object
System.out.println(s1 == s3);  // true - interned to same object

// Without interning
String s4 = new String("hello");
System.out.println(s1 == s4);  // false - different objects

How It Works

sequenceDiagram
    participant Client
    participant Factory as FormatFactory
    participant Cache as Format Cache
    participant Format as CharacterFormat
    
    Client->>Factory: getFormat(Arial, 12, black)
    Factory->>Cache: Check cache
    Cache-->>Factory: Not found
    Factory->>Format: Create new format
    Factory->>Cache: Store format
    Factory-->>Client: Format instance
    
    Note over Client: Create Character with format
    
    Client->>Factory: getFormat(Arial, 12, black)
    Factory->>Cache: Check cache
    Cache-->>Factory: Found!
    Factory-->>Client: Same format instance
    
    Note over Client: Millions of characters share one format

Common Mistakes

1. Mutable Intrinsic State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Wrong - flyweight has mutable state
public class CharacterFormat {
    private String color;  // Can be changed!
    
    public void setColor(String color) {
        this.color = color;  // Changes shared state!
    }
}

// One client changes color, all affected!
format.setColor("red");  // All characters using this format turn red

// Right - immutable flyweight
public final class CharacterFormat {
    private final String color;  // Immutable
    // No setters
}

2. Storing Extrinsic State in Flyweight

1
2
3
4
5
6
7
8
9
10
// Wrong - position is extrinsic, shouldn't be shared
public class CharacterFormat {
    private int x, y;  // Position varies per character!
}

// Right - extrinsic state in client
public class Character {
    private int x, y;  // Extrinsic - in client
    private CharacterFormat format;  // Only intrinsic shared
}

3. Not Using Factory

1
2
3
4
5
6
7
8
9
// Wrong - creates duplicates
CharacterFormat f1 = new CharacterFormat("Arial", 12, "black", false, false);
CharacterFormat f2 = new CharacterFormat("Arial", 12, "black", false, false);
// f1 != f2, no sharing

// Right - use factory
CharacterFormat f1 = CharacterFormatFactory.getFormat("Arial", 12, "black", false, false);
CharacterFormat f2 = CharacterFormatFactory.getFormat("Arial", 12, "black", false, false);
// f1 == f2, same object

Real-World Examples

String Pool: JVM interns string literals.

Integer Cache: Java caches Integer objects from -128 to 127.

Font Glyphs: Text rendering systems share glyph data.

Game Trees: Chess engines share board position evaluations.

Singleton ensures one instance. Flyweight can have many shared instances.

Composite structures often use flyweight for leaves.

State objects can be flyweights if they’re stateless.

Wrapping Up

Flyweight shares intrinsic state between objects to save memory. The factory ensures sharing. Clients store extrinsic state.

Use it when you have millions of similar objects and memory is a concern. Make sure intrinsic state is immutable.

The pattern is invisible to most clients. They just get objects from the factory without knowing they’re shared.


Further Reading: