Flyweight Design Pattern
1. What is Flyweight Pattern?
The Flyweight pattern is a GOF Structural Pattern. Flyweight means reducing weight. According to GOF, the flyweight design pattern object is: "Use sharing to support large numbers of fine-grained objects efficiently."
One line: Create minimum objects (if possible one object per class) and keep them in cache memory for reuse, instead of creating new objects every time.
Flyweight design pattern is a structural design pattern like Facade and Adapter. It is used when we need to create a lot of objects of a class. Since every object consumes memory space that can be crucial for low memory devices (mobile devices, embedded systems), the Flyweight pattern reduces the load on memory by sharing objects.
Key concepts: Intrinsic vs Extrinsic properties
To apply Flyweight pattern, we need to divide object properties into intrinsic (sharable) and extrinsic (non-sharable) properties. Intrinsic properties make the object unique. Extrinsic properties are set by client code as method call arguments and used to perform different operations.
| Property Type | Description | Where Stored | Circle Example |
|---|---|---|---|
| Intrinsic (sharable) | Common for all objects or all method calls. Does not change. | Inside the flyweight object (member variable) | label = "circle" |
| Extrinsic (non-sharable) | Unique per context. Given by client app as method parameters. | Passed by client at runtime | radius, fillColor, lineStyle |
Factors before applying Flyweight
- The number of objects to be created by application should be huge
- The object creation is heavy on memory and it can be time-consuming too
- The object properties can be divided into intrinsic and extrinsic properties. Extrinsic properties of an object should be defined by the client program
2. The Problem
Consider a paint application on a smart mobile device. A user can draw shapes: circles, triangles, squares. Mobiles have limited memory. If we create a new object for every shape drawn, the memory fills up fast. Drawing 1000 circles creates 1000 Circle objects. Drawing 1000 squares creates 1000 more. 2000 objects for something that could be served by just 2.
Problem code (without Flyweight)
package com.nt.test;
import com.nt.comp.Circle;
import com.nt.comp.Square;
public class FlyweightProblemDPTest {
public static void main(String[] args) {
// Creating 1000 objects for Circle class
System.out.println("=======Drawing Circle Shapes========");
for (int i = 0; i < 1000; i++) {
Circle circle = new Circle(); // NEW object every time!
circle.draw(i, "red", "dashed");
}
// Creating 1000 objects for Square class
System.out.println("=======Drawing Square Shapes========");
for (int i = 0; i < 1000; i++) {
Square square = new Square(); // NEW object every time!
square.draw(i + 10, "blue", "dashed");
}
// Total: 2000 objects created! Massive memory waste.
}
}
3. The Solution: Flyweight Factory with Cache
Implement a Factory class with caching logic to create one object for each concrete class and to reuse the object by keeping the objects in cache/buffer memory. This is nothing but the Flyweight Pattern.
We need to keep the created objects for Circle, Square in cache memory and reuse those objects to create more Circle shapes and more Square shapes.
Shape.java (Component Interface)
package com.nt.comp;
public interface Shape {
void draw(int arg0, String fillColor, String lineStyle);
}
Circle.java (Concrete class with intrinsic data)
package com.nt.comp;
public class Circle implements Shape {
private String label; // intrinsic data (sharable)
public Circle() {
System.out.println("Circle:: 0-param constructor");
label = "circle";
}
@Override
public void draw(int radius, String fillColor, String lineStyle) {
// extrinsic data passed per call
System.out.println("drawing Circle with radius:" + radius
+ " having fillColor:" + fillColor
+ " and line Style:" + lineStyle);
}
}
Square.java
package com.nt.comp;
public class Square implements Shape {
private String label; // intrinsic data (sharable)
public Square() {
System.out.println("Square:: 0-param constructor");
label = "square";
}
@Override
public void draw(int sideLength, String fillColor, String lineStyle) {
// extrinsic data
System.out.println("drawing Square with side length:" + sideLength
+ " having fillColor:" + fillColor
+ " and line Style:" + lineStyle);
}
}
ShapeFactory.java (Flyweight Factory with HashMap cache)
package com.nt.map;
import java.util.HashMap;
import com.nt.comp.Circle;
import com.nt.comp.Shape;
import com.nt.comp.Square;
public class ShapeFactory {
private static HashMap<String, Shape> cache = new HashMap<>();
public static Shape getShape(String type) {
Shape shape = cache.get(type);
if (shape == null) {
// Object not in cache, create and store it
if (type.equalsIgnoreCase("circle")) {
shape = new Circle();
} else if (type.equalsIgnoreCase("square")) {
shape = new Square();
} else {
throw new IllegalArgumentException("Unknown shape type: " + type);
}
cache.put(type, shape);
System.out.println(type + " object created and cached");
}
return shape; // return cached object
}
}
Solution client code (using ShapeFactory)
package com.nt.test;
import com.nt.comp.Shape;
import com.nt.map.ShapeFactory;
public class FlyweightDPTest {
public static void main(String[] args) {
// Reuses the SAME cached Circle object 1000 times
System.out.println("=======Drawing Circle Shapes========");
for (int i = 0; i < 1000; i++) {
Shape circle = ShapeFactory.getShape("circle");
circle.draw(i, "red", "dotted");
}
// Reuses the SAME cached Square object 1000 times
System.out.println("=======Drawing Square Shapes========");
for (int i = 0; i < 1000; i++) {
Shape square = ShapeFactory.getShape("square");
square.draw(i + 10, "green", "solid");
}
// Total: only 2 objects created for 2000 draw calls!
}
}
4. Flyweight in Spring Framework
The IOC container of Spring manages the singleton scope spring bean class objects in the internal cache of the IOC container for reusability. So we can say there is Flyweight DP in Spring.
When you call factory.getBean("myService") multiple times for a singleton-scoped bean, Spring returns the same cached object every time. This is the Flyweight pattern applied at the framework level.
5. Pre-defined Flyweight Examples in Java
| Example | Intrinsic (cached) | Extrinsic (per use) |
|---|---|---|
String Pool | String literal value. "hello" is created once in the pool. | Reference variable name |
Integer.valueOf() | Integer objects for values -128 to 127 are cached | Variable assignment context |
Boolean.TRUE / FALSE | Two Boolean objects cached | Usage context |
Character.valueOf() | Characters 0-127 cached | Usage context |
| Spring Singleton Beans | Bean instance in IOC container | Method parameters per request |
// String Pool as Flyweight
String s1 = "hello"; // created in String Pool
String s2 = "hello"; // reuses the SAME object from pool
System.out.println(s1 == s2); // true (same object)
String s3 = new String("hello"); // new object on heap (bypasses pool)
System.out.println(s1 == s3); // false (different objects)
// Integer Cache as Flyweight
Integer a = Integer.valueOf(100); // cached
Integer b = Integer.valueOf(100); // reuses cached
System.out.println(a == b); // true (same object, within -128 to 127)
Integer c = Integer.valueOf(200); // NOT cached (outside range)
Integer d = Integer.valueOf(200); // new object
System.out.println(c == d); // false (different objects)
6. Full-Length Industry Project: Game Sprite Rendering Engine
A 2D game renders a forest scene with thousands of trees, rocks, and bushes. Each tree type (Oak, Pine, Birch) has the same texture, mesh data, and colour (intrinsic properties that consume heavy memory). But each tree instance is placed at a different position (x, y) with a different scale (extrinsic properties that are lightweight). Without Flyweight, rendering 10,000 trees would load 10,000 copies of the same texture. With Flyweight, we load 3 textures (one per tree type) and share them across all 10,000 instances.
High-Level Design (HLD)
Low-Level Design (LLD)
Project Structure
Sprite.java (Flyweight interface)
package com.game.forest.sprite;
public interface Sprite {
void render(int x, int y, double scale);
String getType();
int getMemorySize();
}
TreeSprite.java (Concrete flyweight)
package com.game.forest.sprite;
public class TreeSprite implements Sprite {
private final String type; // intrinsic
private final String texture; // intrinsic (heavy: simulates loaded texture data)
private final String meshData; // intrinsic (heavy: simulates 3D mesh)
private final String baseColor; // intrinsic
public TreeSprite(String type) {
this.type = type;
// Simulate loading heavy texture and mesh data
this.texture = "texture_" + type + "_2048x2048px_RGB";
this.meshData = "mesh_" + type + "_15000_polygons";
this.baseColor = type.contains("pine") ? "dark_green" : "green";
System.out.println("TreeSprite created: " + type
+ " (texture: " + getMemorySize() + " KB loaded)");
}
@Override
public void render(int x, int y, double scale) {
System.out.println(" Render " + type + " at (" + x + "," + y
+ ") scale:" + scale + " [shared texture:" + texture + "]");
}
@Override
public String getType() { return type; }
@Override
public int getMemorySize() { return 2048; }
}
RockSprite.java
package com.game.forest.sprite;
public class RockSprite implements Sprite {
private final String type;
private final String texture;
private final String meshData;
public RockSprite(String type) {
this.type = type;
this.texture = "texture_" + type + "_1024x1024px";
this.meshData = "mesh_" + type + "_5000_polygons";
System.out.println("RockSprite created: " + type
+ " (texture: " + getMemorySize() + " KB loaded)");
}
@Override
public void render(int x, int y, double scale) {
System.out.println(" Render " + type + " at (" + x + "," + y
+ ") scale:" + scale);
}
@Override
public String getType() { return type; }
@Override
public int getMemorySize() { return 1024; }
}
BushSprite.java
package com.game.forest.sprite;
public class BushSprite implements Sprite {
private final String type;
private final String texture;
public BushSprite(String type) {
this.type = type;
this.texture = "texture_" + type + "_512x512px";
System.out.println("BushSprite created: " + type
+ " (texture: " + getMemorySize() + " KB loaded)");
}
@Override
public void render(int x, int y, double scale) {
System.out.println(" Render " + type + " at (" + x + "," + y
+ ") scale:" + scale);
}
@Override
public String getType() { return type; }
@Override
public int getMemorySize() { return 512; }
}
SpriteFactory.java (Flyweight Factory with cache)
package com.game.forest.factory;
import com.game.forest.sprite.*;
import java.util.HashMap;
import java.util.Map;
public class SpriteFactory {
private static final Map<String, Sprite> cache = new HashMap<>();
private static int cacheHits = 0;
private static int cacheMisses = 0;
public static Sprite getSprite(String type) {
Sprite sprite = cache.get(type);
if (sprite != null) {
cacheHits++;
return sprite;
}
// Cache miss: create new sprite
cacheMisses++;
switch (type) {
case "oak_tree":
case "pine_tree":
case "birch_tree":
sprite = new TreeSprite(type);
break;
case "small_rock":
case "large_rock":
sprite = new RockSprite(type);
break;
case "green_bush":
case "flower_bush":
sprite = new BushSprite(type);
break;
default:
throw new IllegalArgumentException("Unknown sprite type: " + type);
}
cache.put(type, sprite);
return sprite;
}
public static int getCacheSize() {
return cache.size();
}
public static String getCacheStats() {
int totalMemory = cache.values().stream()
.mapToInt(Sprite::getMemorySize).sum();
return "Cache: " + cache.size() + " sprites | "
+ "Hits: " + cacheHits + " | Misses: " + cacheMisses + " | "
+ "Memory: " + totalMemory + " KB";
}
}
SceneObject.java (holds extrinsic state)
package com.game.forest.scene;
import com.game.forest.sprite.Sprite;
public class SceneObject {
private final Sprite sprite; // shared flyweight reference
private final int x; // extrinsic: position
private final int y; // extrinsic: position
private final double scale; // extrinsic: size
public SceneObject(Sprite sprite, int x, int y, double scale) {
this.sprite = sprite;
this.x = x;
this.y = y;
this.scale = scale;
}
public void render() {
sprite.render(x, y, scale);
}
}
GameEngine.java
package com.game.forest.engine;
import com.game.forest.factory.SpriteFactory;
import com.game.forest.scene.SceneObject;
import com.game.forest.sprite.Sprite;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class GameEngine {
private final List<SceneObject> sceneObjects = new ArrayList<>();
private final Random random = new Random(42);
private static final String[] SPRITE_TYPES = {
"oak_tree", "pine_tree", "birch_tree",
"small_rock", "large_rock",
"green_bush", "flower_bush"
};
public void generateForest(int objectCount) {
System.out.println("Generating forest with " + objectCount + " objects...\n");
for (int i = 0; i < objectCount; i++) {
String type = SPRITE_TYPES[random.nextInt(SPRITE_TYPES.length)];
Sprite sprite = SpriteFactory.getSprite(type);
int x = random.nextInt(1920);
int y = random.nextInt(1080);
double scale = 0.5 + random.nextDouble() * 1.5;
sceneObjects.add(new SceneObject(sprite, x, y, scale));
}
}
public void renderScene(int maxRender) {
System.out.println("\nRendering first " + maxRender + " objects:");
for (int i = 0; i < Math.min(maxRender, sceneObjects.size()); i++) {
sceneObjects.get(i).render();
}
}
public void printStats(int totalObjects) {
System.out.println("\n=== Performance Stats ===");
System.out.println("Total scene objects: " + totalObjects);
System.out.println("Unique sprites created: " + SpriteFactory.getCacheSize());
System.out.println(SpriteFactory.getCacheStats());
// Without flyweight
int avgSpriteSize = 1500; // average KB per sprite
long withoutFlyweight = (long) totalObjects * avgSpriteSize;
int withFlyweight = SpriteFactory.getCacheSize() * avgSpriteSize;
System.out.println("\nMemory WITHOUT Flyweight: " + withoutFlyweight + " KB ("
+ (withoutFlyweight / 1024) + " MB)");
System.out.println("Memory WITH Flyweight: " + withFlyweight + " KB ("
+ (withFlyweight / 1024) + " MB)");
System.out.println("Memory saved: "
+ ((withoutFlyweight - withFlyweight) * 100 / withoutFlyweight) + "%");
}
}
Application.java
package com.game.forest;
import com.game.forest.engine.GameEngine;
public class Application {
public static void main(String[] args) {
GameEngine engine = new GameEngine();
int totalObjects = 10000;
engine.generateForest(totalObjects);
engine.renderScene(10);
engine.printStats(totalObjects);
}
}
Expected Output
Key observations from the project
- 7 sprite objects serve 10,000 scene objects. Only 7 constructors called (one per sprite type). 9,993 cache hits. 99% memory saved.
- Intrinsic state (texture, mesh, colour) is shared. All oak trees use the same 2048 KB texture. Loading it 10,000 times would consume 14+ GB of memory.
- Extrinsic state (x, y, scale) is stored in SceneObject. Each of the 10,000 scene objects holds only 3 lightweight values (16 bytes each), not the heavy texture data.
- SpriteFactory acts as the Flyweight Factory. It checks the cache first. On miss, it creates and caches. On hit, it returns the cached object. The client never calls
newdirectly. - Cache stats provide visibility. The factory tracks hits, misses, and total memory. This is useful for monitoring in production games.
7. When to Use and When NOT to Use
When to use Flyweight
| Scenario | Example | Why Flyweight |
|---|---|---|
| Large number of similar objects | Game sprites, document characters, map tiles | Share common data, pass unique data per use |
| Memory-constrained environments | Mobile apps, embedded systems, IoT | Reduce object count dramatically |
| Object creation is expensive | Loading textures, parsing configs, DB connections | Create once, reuse many times via cache |
| Most properties can be extrinsic | Position, size, color passed per call | Shared object + external context = many virtual objects |
| Caching and pooling | String pool, Integer cache, connection pool | JVM and frameworks use Flyweight internally |
When NOT to use Flyweight
| Scenario | Why NOT | Use Instead |
|---|---|---|
| Few objects needed | Caching overhead not worth it for 10 objects | Direct creation |
| Each object is unique | No intrinsic state to share | Normal objects |
| Thread safety required | Shared mutable state needs synchronization, adding complexity | Immutable objects or thread-local |
8. Flyweight vs Singleton vs Object Pool
| Aspect | Flyweight | Singleton | Object Pool |
|---|---|---|---|
| Objects | One per type (many types cached) | One per class (only one instance) | Fixed pool of reusable objects |
| Cache | HashMap of shared objects | Single static reference | Queue/list of available objects |
| State | Intrinsic shared + extrinsic per call | All state in one object | Each object has its own state |
| Use case | Many similar objects (shapes, sprites) | One global instance (config, logger) | Expensive objects (DB connections, threads) |
| JDK example | String pool, Integer cache | Runtime.getRuntime() | Thread pool (ExecutorService) |
9. Quick Reference Card
| Component | Role | In Game Project | In Shape Example |
|---|---|---|---|
| Flyweight Interface | Common type for shared objects | Sprite | Shape |
| Concrete Flyweight | Shared object with intrinsic state | TreeSprite, RockSprite | Circle, Square |
| Flyweight Factory | Creates and caches flyweight objects | SpriteFactory | ShapeFactory |
| Extrinsic State | Per-use data passed by client | SceneObject (x, y, scale) | Method params (radius, color) |
| Intrinsic State | Shared data stored in flyweight | texture, meshData, baseColor | label ("circle"/"square") |
Flyweight Design Pattern in Java
1. What is Flyweight Pattern?
The Flyweight pattern is a GOF Structural Pattern. Flyweight means reducing weight. According to GOF, the flyweight design pattern object is: "Use sharing to support large numbers of fine-grained objects efficiently."
One line: Create minimum objects (if possible one object per class) and keep them in cache memory for reuse, instead of creating new objects every time.
Flyweight design pattern is a structural design pattern like Facade and Adapter. It is used when we need to create a lot of objects of a class. Since every object consumes memory space that can be crucial for low memory devices (mobile devices, embedded systems), the Flyweight pattern reduces the load on memory by sharing objects.
Key concepts: Intrinsic vs Extrinsic properties
To apply Flyweight pattern, we need to divide object properties into intrinsic (sharable) and extrinsic (non-sharable) properties. Intrinsic properties make the object unique. Extrinsic properties are set by client code as method call arguments and used to perform different operations.
| Property Type | Description | Where Stored | Circle Example |
|---|---|---|---|
| Intrinsic (sharable) | Common for all objects or all method calls. Does not change. | Inside the flyweight object (member variable) | label = "circle" |
| Extrinsic (non-sharable) | Unique per context. Given by client app as method parameters. | Passed by client at runtime | radius, fillColor, lineStyle |
Factors before applying Flyweight
- The number of objects to be created by application should be huge
- The object creation is heavy on memory and it can be time-consuming too
- The object properties can be divided into intrinsic and extrinsic properties. Extrinsic properties of an object should be defined by the client program
2. The Problem
Consider a paint application on a smart mobile device. A user can draw shapes: circles, triangles, squares. Mobiles have limited memory. If we create a new object for every shape drawn, the memory fills up fast. Drawing 1000 circles creates 1000 Circle objects. Drawing 1000 squares creates 1000 more. 2000 objects for something that could be served by just 2.
Problem code (without Flyweight)
package com.nt.test;
import com.nt.comp.Circle;
import com.nt.comp.Square;
public class FlyweightProblemDPTest {
public static void main(String[] args) {
// Creating 1000 objects for Circle class
System.out.println("=======Drawing Circle Shapes========");
for (int i = 0; i < 1000; i++) {
Circle circle = new Circle(); // NEW object every time!
circle.draw(i, "red", "dashed");
}
// Creating 1000 objects for Square class
System.out.println("=======Drawing Square Shapes========");
for (int i = 0; i < 1000; i++) {
Square square = new Square(); // NEW object every time!
square.draw(i + 10, "blue", "dashed");
}
// Total: 2000 objects created! Massive memory waste.
}
}
3. The Solution: Flyweight Factory with Cache
Implement a Factory class with caching logic to create one object for each concrete class and to reuse the object by keeping the objects in cache/buffer memory. This is nothing but the Flyweight Pattern.
We need to keep the created objects for Circle, Square in cache memory and reuse those objects to create more Circle shapes and more Square shapes.
Shape.java (Component Interface)
package com.nt.comp;
public interface Shape {
void draw(int arg0, String fillColor, String lineStyle);
}
Circle.java (Concrete class with intrinsic data)
package com.nt.comp;
public class Circle implements Shape {
private String label; // intrinsic data (sharable)
public Circle() {
System.out.println("Circle:: 0-param constructor");
label = "circle";
}
@Override
public void draw(int radius, String fillColor, String lineStyle) {
// extrinsic data passed per call
System.out.println("drawing Circle with radius:" + radius
+ " having fillColor:" + fillColor
+ " and line Style:" + lineStyle);
}
}
Square.java
package com.nt.comp;
public class Square implements Shape {
private String label; // intrinsic data (sharable)
public Square() {
System.out.println("Square:: 0-param constructor");
label = "square";
}
@Override
public void draw(int sideLength, String fillColor, String lineStyle) {
// extrinsic data
System.out.println("drawing Square with side length:" + sideLength
+ " having fillColor:" + fillColor
+ " and line Style:" + lineStyle);
}
}
ShapeFactory.java (Flyweight Factory with HashMap cache)
package com.nt.map;
import java.util.HashMap;
import com.nt.comp.Circle;
import com.nt.comp.Shape;
import com.nt.comp.Square;
public class ShapeFactory {
private static HashMap<String, Shape> cache = new HashMap<>();
public static Shape getShape(String type) {
Shape shape = cache.get(type);
if (shape == null) {
// Object not in cache, create and store it
if (type.equalsIgnoreCase("circle")) {
shape = new Circle();
} else if (type.equalsIgnoreCase("square")) {
shape = new Square();
} else {
throw new IllegalArgumentException("Unknown shape type: " + type);
}
cache.put(type, shape);
System.out.println(type + " object created and cached");
}
return shape; // return cached object
}
}
Solution client code (using ShapeFactory)
package com.nt.test;
import com.nt.comp.Shape;
import com.nt.map.ShapeFactory;
public class FlyweightDPTest {
public static void main(String[] args) {
// Reuses the SAME cached Circle object 1000 times
System.out.println("=======Drawing Circle Shapes========");
for (int i = 0; i < 1000; i++) {
Shape circle = ShapeFactory.getShape("circle");
circle.draw(i, "red", "dotted");
}
// Reuses the SAME cached Square object 1000 times
System.out.println("=======Drawing Square Shapes========");
for (int i = 0; i < 1000; i++) {
Shape square = ShapeFactory.getShape("square");
square.draw(i + 10, "green", "solid");
}
// Total: only 2 objects created for 2000 draw calls!
}
}
4. Flyweight in Spring Framework
The IOC container of Spring manages the singleton scope spring bean class objects in the internal cache of the IOC container for reusability. So we can say there is Flyweight DP in Spring.
When you call factory.getBean("myService") multiple times for a singleton-scoped bean, Spring returns the same cached object every time. This is the Flyweight pattern applied at the framework level.
5. Pre-defined Flyweight Examples in Java
| Example | Intrinsic (cached) | Extrinsic (per use) |
|---|---|---|
String Pool | String literal value. "hello" is created once in the pool. | Reference variable name |
Integer.valueOf() | Integer objects for values -128 to 127 are cached | Variable assignment context |
Boolean.TRUE / FALSE | Two Boolean objects cached | Usage context |
Character.valueOf() | Characters 0-127 cached | Usage context |
| Spring Singleton Beans | Bean instance in IOC container | Method parameters per request |
// String Pool as Flyweight
String s1 = "hello"; // created in String Pool
String s2 = "hello"; // reuses the SAME object from pool
System.out.println(s1 == s2); // true (same object)
String s3 = new String("hello"); // new object on heap (bypasses pool)
System.out.println(s1 == s3); // false (different objects)
// Integer Cache as Flyweight
Integer a = Integer.valueOf(100); // cached
Integer b = Integer.valueOf(100); // reuses cached
System.out.println(a == b); // true (same object, within -128 to 127)
Integer c = Integer.valueOf(200); // NOT cached (outside range)
Integer d = Integer.valueOf(200); // new object
System.out.println(c == d); // false (different objects)
6. Full-Length Industry Project: Game Sprite Rendering Engine
A 2D game renders a forest scene with thousands of trees, rocks, and bushes. Each tree type (Oak, Pine, Birch) has the same texture, mesh data, and colour (intrinsic properties that consume heavy memory). But each tree instance is placed at a different position (x, y) with a different scale (extrinsic properties that are lightweight). Without Flyweight, rendering 10,000 trees would load 10,000 copies of the same texture. With Flyweight, we load 3 textures (one per tree type) and share them across all 10,000 instances.
High-Level Design (HLD)
Low-Level Design (LLD)
Project Structure
Sprite.java (Flyweight interface)
package com.game.forest.sprite;
public interface Sprite {
void render(int x, int y, double scale);
String getType();
int getMemorySize();
}
TreeSprite.java (Concrete flyweight)
package com.game.forest.sprite;
public class TreeSprite implements Sprite {
private final String type; // intrinsic
private final String texture; // intrinsic (heavy: simulates loaded texture data)
private final String meshData; // intrinsic (heavy: simulates 3D mesh)
private final String baseColor; // intrinsic
public TreeSprite(String type) {
this.type = type;
// Simulate loading heavy texture and mesh data
this.texture = "texture_" + type + "_2048x2048px_RGB";
this.meshData = "mesh_" + type + "_15000_polygons";
this.baseColor = type.contains("pine") ? "dark_green" : "green";
System.out.println("TreeSprite created: " + type
+ " (texture: " + getMemorySize() + " KB loaded)");
}
@Override
public void render(int x, int y, double scale) {
System.out.println(" Render " + type + " at (" + x + "," + y
+ ") scale:" + scale + " [shared texture:" + texture + "]");
}
@Override
public String getType() { return type; }
@Override
public int getMemorySize() { return 2048; }
}
RockSprite.java
package com.game.forest.sprite;
public class RockSprite implements Sprite {
private final String type;
private final String texture;
private final String meshData;
public RockSprite(String type) {
this.type = type;
this.texture = "texture_" + type + "_1024x1024px";
this.meshData = "mesh_" + type + "_5000_polygons";
System.out.println("RockSprite created: " + type
+ " (texture: " + getMemorySize() + " KB loaded)");
}
@Override
public void render(int x, int y, double scale) {
System.out.println(" Render " + type + " at (" + x + "," + y
+ ") scale:" + scale);
}
@Override
public String getType() { return type; }
@Override
public int getMemorySize() { return 1024; }
}
BushSprite.java
package com.game.forest.sprite;
public class BushSprite implements Sprite {
private final String type;
private final String texture;
public BushSprite(String type) {
this.type = type;
this.texture = "texture_" + type + "_512x512px";
System.out.println("BushSprite created: " + type
+ " (texture: " + getMemorySize() + " KB loaded)");
}
@Override
public void render(int x, int y, double scale) {
System.out.println(" Render " + type + " at (" + x + "," + y
+ ") scale:" + scale);
}
@Override
public String getType() { return type; }
@Override
public int getMemorySize() { return 512; }
}
SpriteFactory.java (Flyweight Factory with cache)
package com.game.forest.factory;
import com.game.forest.sprite.*;
import java.util.HashMap;
import java.util.Map;
public class SpriteFactory {
private static final Map<String, Sprite> cache = new HashMap<>();
private static int cacheHits = 0;
private static int cacheMisses = 0;
public static Sprite getSprite(String type) {
Sprite sprite = cache.get(type);
if (sprite != null) {
cacheHits++;
return sprite;
}
// Cache miss: create new sprite
cacheMisses++;
switch (type) {
case "oak_tree":
case "pine_tree":
case "birch_tree":
sprite = new TreeSprite(type);
break;
case "small_rock":
case "large_rock":
sprite = new RockSprite(type);
break;
case "green_bush":
case "flower_bush":
sprite = new BushSprite(type);
break;
default:
throw new IllegalArgumentException("Unknown sprite type: " + type);
}
cache.put(type, sprite);
return sprite;
}
public static int getCacheSize() {
return cache.size();
}
public static String getCacheStats() {
int totalMemory = cache.values().stream()
.mapToInt(Sprite::getMemorySize).sum();
return "Cache: " + cache.size() + " sprites | "
+ "Hits: " + cacheHits + " | Misses: " + cacheMisses + " | "
+ "Memory: " + totalMemory + " KB";
}
}
SceneObject.java (holds extrinsic state)
package com.game.forest.scene;
import com.game.forest.sprite.Sprite;
public class SceneObject {
private final Sprite sprite; // shared flyweight reference
private final int x; // extrinsic: position
private final int y; // extrinsic: position
private final double scale; // extrinsic: size
public SceneObject(Sprite sprite, int x, int y, double scale) {
this.sprite = sprite;
this.x = x;
this.y = y;
this.scale = scale;
}
public void render() {
sprite.render(x, y, scale);
}
}
GameEngine.java
package com.game.forest.engine;
import com.game.forest.factory.SpriteFactory;
import com.game.forest.scene.SceneObject;
import com.game.forest.sprite.Sprite;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class GameEngine {
private final List<SceneObject> sceneObjects = new ArrayList<>();
private final Random random = new Random(42);
private static final String[] SPRITE_TYPES = {
"oak_tree", "pine_tree", "birch_tree",
"small_rock", "large_rock",
"green_bush", "flower_bush"
};
public void generateForest(int objectCount) {
System.out.println("Generating forest with " + objectCount + " objects...\n");
for (int i = 0; i < objectCount; i++) {
String type = SPRITE_TYPES[random.nextInt(SPRITE_TYPES.length)];
Sprite sprite = SpriteFactory.getSprite(type);
int x = random.nextInt(1920);
int y = random.nextInt(1080);
double scale = 0.5 + random.nextDouble() * 1.5;
sceneObjects.add(new SceneObject(sprite, x, y, scale));
}
}
public void renderScene(int maxRender) {
System.out.println("\nRendering first " + maxRender + " objects:");
for (int i = 0; i < Math.min(maxRender, sceneObjects.size()); i++) {
sceneObjects.get(i).render();
}
}
public void printStats(int totalObjects) {
System.out.println("\n=== Performance Stats ===");
System.out.println("Total scene objects: " + totalObjects);
System.out.println("Unique sprites created: " + SpriteFactory.getCacheSize());
System.out.println(SpriteFactory.getCacheStats());
// Without flyweight
int avgSpriteSize = 1500; // average KB per sprite
long withoutFlyweight = (long) totalObjects * avgSpriteSize;
int withFlyweight = SpriteFactory.getCacheSize() * avgSpriteSize;
System.out.println("\nMemory WITHOUT Flyweight: " + withoutFlyweight + " KB ("
+ (withoutFlyweight / 1024) + " MB)");
System.out.println("Memory WITH Flyweight: " + withFlyweight + " KB ("
+ (withFlyweight / 1024) + " MB)");
System.out.println("Memory saved: "
+ ((withoutFlyweight - withFlyweight) * 100 / withoutFlyweight) + "%");
}
}
Application.java
package com.game.forest;
import com.game.forest.engine.GameEngine;
public class Application {
public static void main(String[] args) {
GameEngine engine = new GameEngine();
int totalObjects = 10000;
engine.generateForest(totalObjects);
engine.renderScene(10);
engine.printStats(totalObjects);
}
}
Expected Output
Key observations from the project
- 7 sprite objects serve 10,000 scene objects. Only 7 constructors called (one per sprite type). 9,993 cache hits. 99% memory saved.
- Intrinsic state (texture, mesh, colour) is shared. All oak trees use the same 2048 KB texture. Loading it 10,000 times would consume 14+ GB of memory.
- Extrinsic state (x, y, scale) is stored in SceneObject. Each of the 10,000 scene objects holds only 3 lightweight values (16 bytes each), not the heavy texture data.
- SpriteFactory acts as the Flyweight Factory. It checks the cache first. On miss, it creates and caches. On hit, it returns the cached object. The client never calls
newdirectly. - Cache stats provide visibility. The factory tracks hits, misses, and total memory. This is useful for monitoring in production games.
7. When to Use and When NOT to Use
When to use Flyweight
| Scenario | Example | Why Flyweight |
|---|---|---|
| Large number of similar objects | Game sprites, document characters, map tiles | Share common data, pass unique data per use |
| Memory-constrained environments | Mobile apps, embedded systems, IoT | Reduce object count dramatically |
| Object creation is expensive | Loading textures, parsing configs, DB connections | Create once, reuse many times via cache |
| Most properties can be extrinsic | Position, size, color passed per call | Shared object + external context = many virtual objects |
| Caching and pooling | String pool, Integer cache, connection pool | JVM and frameworks use Flyweight internally |
When NOT to use Flyweight
| Scenario | Why NOT | Use Instead |
|---|---|---|
| Few objects needed | Caching overhead not worth it for 10 objects | Direct creation |
| Each object is unique | No intrinsic state to share | Normal objects |
| Thread safety required | Shared mutable state needs synchronization, adding complexity | Immutable objects or thread-local |
8. Flyweight vs Singleton vs Object Pool
| Aspect | Flyweight | Singleton | Object Pool |
|---|---|---|---|
| Objects | One per type (many types cached) | One per class (only one instance) | Fixed pool of reusable objects |
| Cache | HashMap of shared objects | Single static reference | Queue/list of available objects |
| State | Intrinsic shared + extrinsic per call | All state in one object | Each object has its own state |
| Use case | Many similar objects (shapes, sprites) | One global instance (config, logger) | Expensive objects (DB connections, threads) |
| JDK example | String pool, Integer cache | Runtime.getRuntime() | Thread pool (ExecutorService) |
9. Quick Reference Card
| Component | Role | In Game Project | In Shape Example |
|---|---|---|---|
| Flyweight Interface | Common type for shared objects | Sprite | Shape |
| Concrete Flyweight | Shared object with intrinsic state | TreeSprite, RockSprite | Circle, Square |
| Flyweight Factory | Creates and caches flyweight objects | SpriteFactory | ShapeFactory |
| Extrinsic State | Per-use data passed by client | SceneObject (x, y, scale) | Method params (radius, color) |
| Intrinsic State | Shared data stored in flyweight | texture, meshData, baseColor | label ("circle"/"square") |