LLD - Flyweight Design Pattern


Flyweight Design Pattern
This is a structural design pattern that is used to minimize memory usage by sharing as much data as possible with similar objects, instead of creating new instances for every object.
Shares intrinsic (internal, immutable) state between objects.
Keeps extrinsic (external, context-specific) state outside the shared object.
Ideal for scenarios where you need to create thousands or millions of similar objects.
Helps with performance optimization, especially in memory-constrained environments.
Problem Statement
Let’s say we are building a forest simulation with 1 million trees. Each Tree
has:
class Tree {
String type;
String color;
String texture;
int x; // x-coordinate
int y; // y-coordinate
}
Each Tree
object has:
Property | Type | Size (approx) |
type | String | 40 bytes |
color | String | 40 bytes |
texture | String | 40 bytes |
x coordinate | int | 4 bytes |
y coordinate | int | 4 bytes |
Total memory per tree = 40 + 40 + 40 + 4 + 4 = 128 bytes
Total memory used for 1 Million trees:
128 bytes × 1,000,000 trees = ~128 MB
That's 128 MB just for storing tree data, even though most trees have the same type/color/texture.
Redundant memory allocation for intrinsic data, i.e, creating
Tree
object.Slower performance due to memory bloat.
Solution
To share common parts (intrinsic state) of an object between multiple instances to reduce memory consumption using the Flyweight Design Pattern
Blueprint of Flyweight Design Pattern
Flyweight: The interface or abstract class that defines the method(s) to receive extrinsic state and operate on it.
ConcreteFlyweight: Implements the Flyweight interface. Stores the intrinsic (shared) state. Stateless from a client’s point of view.
FlyweightFactory: A factory that creates and manages a pool (cache) of shared flyweight objects. Ensures reuse.
Client: Uses Flyweight and passes extrinsic state.
Client ----> FlyweightFactory ----> Flyweight (interface)
|
+--> ConcreteFlyweight
Github - Flyweight Design Pattern Complete Code
interface TreeType {
void draw(int x, int y);
}
record ConcreteTreeType(String name, String color, String texture) implements TreeType {
@Override
public void draw(int x, int y) {
System.out.printf("Drawing %s tree at (%d, %d) with color %s and texture %s%n",
name, x, y, color, texture);
}
}
class TreeFactory {
private static final Map<String, TreeType> treeTypes = new HashMap<>();
public static TreeType getTreeType(String name, String color, String texture) {
var key = name + ":" + color + ":" + texture;
return treeTypes.computeIfAbsent(key, k -> new ConcreteTreeType(name, color, texture));
}
public static int totalTreeTypes() {
return treeTypes.size();
}
}
// Immutable Context class (extrinsic state)
record Tree(int x, int y, TreeType type) {
public void draw() {
type.draw(x, y);
}
}
import java.util.ArrayList;
import java.util.List;
public class Forest {
private final List<Tree> trees = new ArrayList<>();
public void plantTree(int x, int y, String name, String color, String texture) {
TreeType type = TreeFactory.getTreeType(name, color, texture);
Tree tree = new Tree(x, y, type);
trees.add(tree);
}
public void drawForest() {
trees.forEach(Tree::draw);
}
public int totalTreesPlanted() {
return trees.size();
}
}
import java.util.Random;
public class Main {
private static final String[] treeTypes = {"Oak", "Pine", "Mango"};
private static final String[] colors = {"Green", "Dark Green", "Yellow"};
private static final String[] textures = {"Smooth", "Rough"};
public static void main(String[] args) {
Forest forest = new Forest();
Random random = new Random();
int treeCount = 10000;
for (int i = 0; i < treeCount; i++) {
String type = treeTypes[random.nextInt(treeTypes.length)];
String color = colors[random.nextInt(colors.length)];
String texture = textures[random.nextInt(textures.length)];
int x = random.nextInt(1000);
int y = random.nextInt(1000);
forest.plantTree(x, y, type, color, texture);
}
forest.drawForest();
System.out.println("\n--- Forest Summary ---");
System.out.println("Total trees planted: " + forest.totalTreesPlanted());
System.out.println("Total unique TreeTypes: " + TreeFactory.totalTreeTypes());
Runtime runtime = Runtime.getRuntime();
System.out.println("Memory used (MB): " + (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024));
}
}
Case Study - Text Editor
Let’s say you're building a rich text editor where each character can have formatting (font, size, style).
In large documents with millions of characters, storing formatting for each character separately causes high memory usage due to repeated style data.
Optimize memory by sharing common formatting attributes across characters without affecting rendering.
Solution
public interface CharacterGlyph {
void render(int x, int y);
}
public record TwoDCharacterGlyph(String font, int size, String style) implements CharacterGlyph {
@Override
public void render(int x, int y) {
System.out.println("2D Rendering character with font: " + font + ", size: " + size + ", style: " + style + " at (" + x + ", " + y + ")");
}
}
record ThreeDCharacterGlyph(String font, int size, String style) implements CharacterGlyph {
@Override
public void render(int x, int y) {
System.out.println("3D Rendering character with font: " + font + ", size: " + size + ", style: " + style + " at (" + x + ", " + y + ")");
}
}
public class CharacterFactory {
private static final Map<String, CharacterGlyph> characterGlyphCache = new HashMap<>();
// Method to get the glyph based on rendering type (2D or 3D)
public static CharacterGlyph getCharacterGlyph(String font, int size, String style, boolean is3D) {
String key = font + ":" + size + ":" + style + ":" + (is3D ? "3D" : "2D");
if (!characterGlyphCache.containsKey(key)) {
if (is3D) {
characterGlyphCache.put(key, new ThreeDCharacterGlyph(font, size, style));
} else {
characterGlyphCache.put(key, new TwoDCharacterGlyph(font, size, style));
}
}
return characterGlyphCache.get(key);
}
// Get the total number of different glyphs in the cache
public static int totalCharacterGlyphs() {
return characterGlyphCache.size();
}
}
public class Main {
private static final String[] fonts = {"Arial", "Times New Roman", "Courier New"};
private static final String[] styles = {"Bold", "Italic", "Regular"};
private static final Random random = new Random();
public static void main(String[] args) {
int glyphCount = 10000;
for (int i = 0; i < glyphCount; i++) {
String font = fonts[random.nextInt(fonts.length)];
int size = random.nextInt(10) + 10; // Random size between 10 and 20
String style = styles[random.nextInt(styles.length)];
boolean is3D = random.nextBoolean();
// Retrieve or create the character glyph using the flyweight factory
CharacterGlyph glyph = CharacterFactory.getCharacterGlyph(font, size, style, is3D);
// Render the character at random coordinates
int x = random.nextInt(1000);
int y = random.nextInt(1000);
glyph.render(x, y);
}
// Output the number of unique character glyphs used
System.out.println("\n--- Glyphs Summary ---");
System.out.println("Total unique CharacterGlyphs: " + CharacterFactory.totalCharacterGlyphs());
}
}
Benefits
Drastically reduces memory usage by sharing common object data (intrinsic state).
Shared logic (like rendering or styling) is reused and easier to manage.
Subscribe to my newsletter
Read articles from Manish Pushkar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Manish Pushkar
Manish Pushkar
Software Engineer - Backend