Flyweight Design Pattern in Java: Complete Guide with Examples
Learn the Flyweight design pattern in Java with simple explanations and practical examples. This guide covers how to share objects to save memory, with before-and-after code for beginners and experts.
I've been writing Java for years, and Flyweight is the pattern I reach for whenever I see the same object being created thousands of times. Not because creation is slow—because memory runs out long before CPU does. Here's how Flyweight saved me from an out-of-memory crash in a rendering engine.
Why the Flyweight Pattern Still Matters in Java
You've built a game. It works. Then you add a forest of ten thousand trees. Each tree loads its own texture, its own color, its own mesh. Suddenly your application uses 2 GB of memory for objects that look identical. The garbage collector works overtime. The app lags.
The Flyweight pattern solves this by sharing the common parts. All trees of the same species share one flyweight — one texture, one color, one mesh. Only the unique parts (X, Y position) live in the tree instance. Memory drops from 2 GB to 50 MB.
Whether you're building games, text editors, or map renderers, Flyweight keeps your memory footprint small and your application fast.
What is the Flyweight Design Pattern?
"Use sharing to support large numbers of fine-grained objects efficiently."
In plain words: Flyweight shares the state that can be shared, and passes the state that cannot. If ten thousand trees share the same texture, store the texture once. Only the X/Y position — which changes per tree — lives in each instance.
Intrinsic vs Extrinsic State
This is the heart of the pattern. Every object has two kinds of state:
| State Type | Definition | Tree Example |
|---|---|---|
| Intrinsic | Shared, lives in the flyweight | Texture, color, species |
| Extrinsic | Unique per instance, passed in from outside | X position, Y position |
The Three Players
| Role | Responsibility | Example |
|---|---|---|
| Flyweight | Holds the shared intrinsic state | TreeType |
| Flyweight Factory | Caches flyweights and returns the same instance for the same key | TreeTypeFactory |
| Context | Holds extrinsic state and a reference to the flyweight | Tree (x, y, flyweight) |
Flyweight pattern structure: 10,000 trees each hold x, y coordinates plus a reference to ONE shared TreeType object.
How to read this diagram (for freshers): The top row shows the flow — a
Client (the Forest) asks the Factory for a tree type.
The factory returns a shared Flyweight — one object that holds the
texture, color, and species. The bottom row shows Contexts — thousands
of individual Tree objects, each holding only its own X/Y position and a reference to the
shared flyweight. When a Tree is drawn, it passes its X/Y to the shared flyweight's
draw() method. The texture lives in one place. The positions live everywhere.
Real-World Example for Freshers
Before we touch code, let's look at the Flyweight pattern you already use every day.
Example 1: A Video Game Forest
Imagine an open-world game with a forest of 100,000 trees. Each tree has:
- A texture (an image file, ~50 KB each)
- A color
- A species name
- An X position
- A Y position
If each tree loaded its own texture, that's 100,000 × 50 KB = 5 GB of RAM just for tree textures. The game would crash.
With Flyweight, there are only 5 species. So only 5 textures are loaded. That's 250 KB total. Each of the 100,000 trees holds only 16 bytes for X, Y plus a reference to its species. The memory drops from 5 GB to a few megabytes.
That's the Flyweight pattern. Share the state that's identical. Keep the state that's unique.
Example 2: Java String Pool (You Already Use This)
When you write this in Java:
String a = "hello";
String b = "hello";
System.out.println(a == b); // true
Both a and b point to the same string object in
memory. The JVM keeps a "string pool" of flyweights. When you write "hello"
the second time, the JVM reuses the first one instead of creating a new string.
That's Flyweight in the standard library.
Example 3: A Text Editor
Every character in a text document has a position (line, column), but every "a" in the document shares the same glyph (the shape and font metrics for the letter "a"). The editor stores one glyph per character per font, not one glyph per character occurrence.
A book with 500,000 characters and only 128 distinct characters uses 128 glyph objects, not 500,000. That's Flyweight.
Quick Summary for Freshers
| Real-World Situation | Shared (Intrinsic) | Unique (Extrinsic) |
|---|---|---|
| Game forest | Texture, color, species | X, Y position |
| Text editor | Glyph, font metrics | Line, column |
| Java String pool | The string content | Variable reference |
| Map tiles | Tile image (water, grass) | Grid position |
Once you see it in a game forest, you'll see it everywhere. That's why Flyweight is one of the most practical design patterns when memory matters.
Violation: The Memory Explosion Nightmare
Imagine you're building the forest. Without Flyweight, every tree loads its own texture:
public class Tree {
private String name; // "Oak", "Pine", etc.
private String texture; // 50 KB image data
private String color;
private int x, y;
public Tree(String name, String texture, String color, int x, int y) {
this.name = name;
this.texture = texture;
this.color = color;
this.x = x;
this.y = y;
}
public void draw() {
System.out.println("Drawing " + name + " at (" + x + "," + y + ")");
}
}
// Creating 10,000 trees
for (int i = 0; i < 10000; i++) {
Tree t = new Tree("Oak", loadTexture("oak.png"), "green",
randomX(), randomY());
forest.add(t);
}
The Problem: The texture field is duplicated in every
tree. 10,000 trees × 50 KB = 500 MB wasted. Add more species and more
trees, and your app crashes with OutOfMemoryError. The X, Y position is the
only thing that truly varies. Everything else is identical for all Oaks.
Solution: Flyweight with Forest and Trees
Let's split each tree into two objects: a shared TreeType (the flyweight)
and a small Tree instance that holds only position + a reference.
Step 1: The Flyweight (Shared State)
public class TreeType {
private final String name;
private final String texture;
private final String color;
public TreeType(String name, String texture, String color) {
this.name = name;
this.texture = texture;
this.color = color;
}
public void draw(int x, int y) {
System.out.println("Drawing " + name + " at (" + x + "," + y + ")");
}
}
Step 2: The Flyweight Factory
import java.util.HashMap;
import java.util.Map;
public class TreeTypeFactory {
private static final Map<String, TreeType> cache = new HashMap<>();
public static TreeType getTreeType(String name, String texture, String color) {
String key = name + "|" + texture + "|" + color;
TreeType type = cache.get(key);
if (type == null) {
type = new TreeType(name, texture, color);
cache.put(key, type);
System.out.println("Created new TreeType: " + key);
}
return type;
}
}
Step 3: The Context (Unique State)
public class Tree {
private final int x;
private final int y;
private final TreeType type; // reference to shared flyweight
public Tree(int x, int y, TreeType type) {
this.x = x;
this.y = y;
this.type = type;
}
public void draw() {
type.draw(x, y);
}
}
Step 4: Using the Flyweight
public class Forest {
public static void main(String[] args) {
// Create 10,000 Oak trees — but only ONE TreeType object for all of them
for (int i = 0; i < 10000; i++) {
TreeType oakType = TreeTypeFactory.getTreeType(
"Oak", "oak_texture_data", "green"
);
Tree oak = new Tree(i, i * 2, oakType);
oak.draw();
}
// Create 5,000 Pine trees — but only ONE TreeType object for all of them
for (int i = 0; i < 5000; i++) {
TreeType pineType = TreeTypeFactory.getTreeType(
"Pine", "pine_texture_data", "dark green"
);
Tree pine = new Tree(i, i * 3, pineType);
pine.draw();
}
}
}
Output (abbreviated):
Created new TreeType: Oak|oak_texture_data|green
Drawing Oak at (0,0)
Drawing Oak at (1,2)
...
Created new TreeType: Pine|pine_texture_data|dark green
Drawing Pine at (0,0)
Drawing Pine at (1,3)
...
What just happened? Only two TreeType
objects were created — one for Oak, one for Pine — even though 15,000 trees exist. The
factory cached each species after the first creation. All 10,000 Oaks share the same
Oak texture. All 5,000 Pines share the same Pine texture. Memory dropped from 750 MB to
under 1 MB for the textures.
For Beginners: The Library Book Analogy
Think of a library with 10,000 patrons. Each patron has a "book they are reading." Without Flyweight, the library would print a fresh copy of every book for every patron. 10,000 patrons × 500 pages = 5,000,000 pages printed.
With Flyweight, the library prints one copy of each book. Each patron holds a "borrow record" that says "Patron X has Book #42 at position 137." The book itself (the flyweight) exists once. The position (the extrinsic state) is per-patron.
Flyweight is the library's one copy. Contexts are the borrow records.
For Experts: Flyweight vs. Singleton (Know the Difference)
This is a common interview trap.
| Aspect | Flyweight | Singleton |
|---|---|---|
| Instances | Many shared instances (one per intrinsic state) | Exactly one instance |
| Purpose | Save memory by sharing | Provide a single global access point |
| Keying | Keyed by intrinsic state | No key — one global instance |
| Focus | Memory | Access control |
Rule of thumb: Flyweight is one per shared species. Singleton is one per application.
Putting It All Together: Real Java Examples
The Flyweight pattern isn't just academic. You use it every day in Java.
Example 1: String Pool
"hello" literals are cached. When you write "hello" a second
time, the JVM returns the same object from the pool. You can verify:
String a = "hello";
String b = "hello";
System.out.println(a == b); // true — same object
Example 2: Integer Cache
Integer.valueOf(100) returns a cached instance for values between -128 and
127. If you call it twice with the same value in that range, you get the same object.
Integer a = Integer.valueOf(100);
Integer b = Integer.valueOf(100);
System.out.println(a == b); // true — cached
Integer c = Integer.valueOf(1000);
Integer d = Integer.valueOf(1000);
System.out.println(c == d); // false — outside cache range
Example 3: Boolean Cache
Boolean.TRUE and Boolean.FALSE are flyweights. There are
exactly two Boolean objects in the JVM, no matter how many booleans you declare.
The Flyweight Structure
| Flyweight Source | How Sharing Works |
|---|---|
String pool |
Identical literals share one object |
Integer.valueOf() |
Values -128 to 127 are cached |
Boolean.TRUE |
Two objects total in the JVM |
Personal note: I once worked on a map rendering service that drew millions of markers on a canvas. Each marker loaded its own icon image (8 KB each). At 1 million markers, that was 8 GB — the service crashed with OOM every few hours. I refactored to Flyweight: one shared icon per marker type (there were only 6 types). The memory dropped from 8 GB to under 100 MB. The service hasn't crashed since.
Frequently Asked Questions
What is the Flyweight design pattern in Java?
Flyweight is a structural pattern that reduces memory usage by sharing common object state across many instances. Instead of creating thousands of identical objects, you create one shared flyweight and pass the unique state externally.
When should I use the Flyweight pattern?
Use Flyweight when your application creates a very large number of similar objects that consume too much memory, and when most of that object state can be shared. It is common in games, text editors, and map rendering.
What is intrinsic vs extrinsic state in Flyweight?
Intrinsic state is stored inside the flyweight and shared across all instances (e.g., a tree's texture). Extrinsic state is passed from outside at call time (e.g., a tree's X/Y position). Flyweight stores only intrinsic state.
How is Flyweight different from Singleton?
Singleton ensures exactly one instance of a class. Flyweight may have many shared instances, each representing a distinct intrinsic state. They both cache objects, but for different reasons: Singleton for global access, Flyweight for memory savings.
Where is Flyweight used in real Java code?
Java's String pool is the classic example — identical string literals share one object.
Integer.valueOf() caches values from -128 to 127. Character
and Boolean also cache commonly used values.
Your Flyweight Checklist
Before creating another thousand objects, ask:
- Am I creating too many similar objects? If memory is the bottleneck, Flyweight helps.
- Can I split the state? If most fields are identical across instances, they belong in a flyweight.
- Do I have a natural key? Flyweight needs a cache key (species name, font name, tile type) to know when to reuse.
- Is the extrinsic state truly per-instance? If X/Y positions change per tree, they stay in the context.
Start small. Refactor one exploding collection. The memory savings compound.
Conclusion
The Flyweight pattern isn't about complexity — it's about shared state. When you create thousands of similar objects and memory becomes the bottleneck, Flyweight gives you one shared instance per intrinsic state and passes the rest externally. It trades a small upfront design decision for long-term memory savings.
I'll be honest: I don't use Flyweight in every project. Small apps with a few hundred objects don't need it. But for anything that touches games, rendering engines, text editors, or map displays — where object counts hit millions — it has saved me from rewriting the same OOM fix over and over. Start with the problem, not the pattern. If you find yourself creating the same object a thousand times just to store the same texture, Flyweight is probably the answer.
Related topics: SOLID Principles in Java, Facade Design Pattern, Decorator Design Pattern, Composite Design Pattern, Structural Design Patterns