Mediumstructural14 min read

Flyweight Design Pattern

1. What is Flyweight Pattern?

Definition

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 TypeDescriptionWhere StoredCircle 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 runtimeradius, fillColor, lineStyle
Circle class with intrinsic/extrinsic separation: public class Circle { private String label; // INTRINSIC (sharable data) label = "circle"; public void draw(int radius, String fillColor, String lineStyle) { // EXTRINSIC (non-sharable data, per call) ... } } Cache (HashMap): Calls: +--------+-----------+ obj.draw(10); O (small circle) | "circle"| Circle obj| obj.draw(20); O (medium circle) +--------+-----------+ obj.draw(30); O (large circle) One object, many uses! All 3 use the SAME cached Circle object

Factors before applying Flyweight

  1. The number of objects to be created by application should be huge
  2. The object creation is heavy on memory and it can be time-consuming too
  3. 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

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.
    }
}
Circle:: constructor Circle:: constructor Circle:: constructor ... (1000 times!) Square:: constructor Square:: constructor ... (1000 times!) Total: 2000 constructor calls, 2000 objects in memory

3. The Solution: Flyweight Factory with Cache

Solution

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!
    }
}
Circle:: 0-param constructor circle object created and cached drawing Circle with radius:0 having fillColor:red and line Style:dotted drawing Circle with radius:1 having fillColor:red and line Style:dotted ... (998 more draws, NO more constructor calls) Square:: 0-param constructor square object created and cached drawing Square with side length:10 having fillColor:green and line Style:solid ... (999 more draws, NO more constructor calls) Total: 2 constructor calls, 2 objects in memory (instead of 2000)
Cache (HashMap<String, Shape>): +----------+--------------------+ | Key | Value | +----------+--------------------+ | "circle" | Circle obj (one) | <-- serves ALL 1000 circle draws | "square" | Square obj (one) | <-- serves ALL 1000 square draws +----------+--------------------+ Memory saved: 1998 objects x ~100 bytes each = ~200 KB saved

4. Flyweight in Spring Framework

Spring and Flyweight

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

ExampleIntrinsic (cached)Extrinsic (per use)
String PoolString literal value. "hello" is created once in the pool.Reference variable name
Integer.valueOf()Integer objects for values -128 to 127 are cachedVariable assignment context
Boolean.TRUE / FALSETwo Boolean objects cachedUsage context
Character.valueOf()Characters 0-127 cachedUsage context
Spring Singleton BeansBean instance in IOC containerMethod 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

Business Scenario

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)

+---------------------------------------------------+ | GAME LOOP | | GameEngine | | - renders forest scene with 10,000 sprites | | - for each sprite: get from factory, draw at pos| +----------------------------+----------------------+ | v +---------------------------------------------------+ | FLYWEIGHT FACTORY | | SpriteFactory | | - cache: HashMap<String, Sprite> | | - getSprite("oak_tree"): Sprite | | - getSprite("pine_tree"): Sprite | | - returns cached instance or creates new | +---------------------------------------------------+ | v +---------------------------------------------------+ | FLYWEIGHT OBJECTS | | Sprite (interface) | | + render(x, y, scale): void | | | | | +----+----+----+ | | | | | | | | OakTree PineTree Birch Rock Bush | | Sprite Sprite Sprite Sprite Sprite | | (intrinsic: texture, mesh, colour) | | (extrinsic: x, y, scale passed per render) | +---------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | Sprite (interface) | +-------------------------------------------------------+ | + render(x: int, y: int, scale: double): void | | + getType(): String | | + getMemorySize(): int (shows texture size in KB) | +-------------------------------------------------------+ ^ ^ ^ | | | +----------------+ +----------------+ +----------------+ | TreeSprite | | RockSprite | | BushSprite | +----------------+ +----------------+ +----------------+ | - type: String | | - type: String | | - type: String | | - texture: String (intrinsic, heavy) | | - meshData: String (intrinsic, heavy) | | - baseColor: String (intrinsic) | +----------------+ +----------------+ +----------------+ +-------------------------------------------------------+ | SpriteFactory (Flyweight Factory) | +-------------------------------------------------------+ | - cache: Map<String, Sprite> [static] | +-------------------------------------------------------+ | + getSprite(type): Sprite [static] | | + getCacheSize(): int [static] | | + getCacheStats(): String [static] | +-------------------------------------------------------+ +-------------------------------------------------------+ | SceneObject (uses flyweight + extrinsic data) | +-------------------------------------------------------+ | - sprite: Sprite [shared flyweight reference] | | - x: int [extrinsic - position] | | - y: int [extrinsic - position] | | - scale: double [extrinsic - size] | +-------------------------------------------------------+ | + render(): void | +-------------------------------------------------------+

Project Structure

forest-game-engine/ ├── src/ │ └── main/java/com/game/forest/ │ ├── sprite/ │ │ ├── Sprite.java (flyweight interface) │ │ ├── TreeSprite.java (concrete flyweight) │ │ ├── RockSprite.java │ │ └── BushSprite.java │ ├── factory/ │ │ └── SpriteFactory.java (flyweight factory with cache) │ ├── scene/ │ │ └── SceneObject.java (extrinsic state holder) │ ├── engine/ │ │ └── GameEngine.java (renders the scene) │ └── Application.java ├── src/ │ └── test/java/com/game/forest/ │ └── factory/ │ └── SpriteFactoryTest.java └── pom.xml

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

Generating forest with 10000 objects... TreeSprite created: pine_tree (texture: 2048 KB loaded) TreeSprite created: oak_tree (texture: 2048 KB loaded) BushSprite created: green_bush (texture: 512 KB loaded) RockSprite created: small_rock (texture: 1024 KB loaded) TreeSprite created: birch_tree (texture: 2048 KB loaded) RockSprite created: large_rock (texture: 1024 KB loaded) BushSprite created: flower_bush (texture: 512 KB loaded) Rendering first 10 objects: Render pine_tree at (1142,503) scale:1.23 [shared texture:texture_pine_tree_2048x2048px_RGB] Render oak_tree at (887,291) scale:0.85 [shared texture:texture_oak_tree_2048x2048px_RGB] Render green_bush at (342,912) scale:1.67 [shared texture:texture_green_bush_512x512px] Render small_rock at (1567,102) scale:0.55 [shared texture:texture_small_rock_1024x1024px] Render pine_tree at (201,775) scale:1.12 [shared texture:texture_pine_tree_2048x2048px_RGB] Render birch_tree at (1890,445) scale:0.92 [shared texture:texture_birch_tree_2048x2048px_RGB] Render oak_tree at (678,623) scale:1.45 [shared texture:texture_oak_tree_2048x2048px_RGB] Render large_rock at (1234,88) scale:0.71 [shared texture:texture_large_rock_1024x1024px] Render flower_bush at (456,999) scale:1.88 [shared texture:texture_flower_bush_512x512px] Render pine_tree at (890,345) scale:1.05 [shared texture:texture_pine_tree_2048x2048px_RGB] === Performance Stats === Total scene objects: 10000 Unique sprites created: 7 Cache: 7 sprites | Hits: 9993 | Misses: 7 | Memory: 9216 KB Memory WITHOUT Flyweight: 15000000 KB (14648 MB) Memory WITH Flyweight: 10500 KB (10 MB) Memory saved: 99%

Key observations from the project

  1. 7 sprite objects serve 10,000 scene objects. Only 7 constructors called (one per sprite type). 9,993 cache hits. 99% memory saved.
  2. 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.
  3. 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.
  4. 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 new directly.
  5. 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

ScenarioExampleWhy Flyweight
Large number of similar objectsGame sprites, document characters, map tilesShare common data, pass unique data per use
Memory-constrained environmentsMobile apps, embedded systems, IoTReduce object count dramatically
Object creation is expensiveLoading textures, parsing configs, DB connectionsCreate once, reuse many times via cache
Most properties can be extrinsicPosition, size, color passed per callShared object + external context = many virtual objects
Caching and poolingString pool, Integer cache, connection poolJVM and frameworks use Flyweight internally

When NOT to use Flyweight

ScenarioWhy NOTUse Instead
Few objects neededCaching overhead not worth it for 10 objectsDirect creation
Each object is uniqueNo intrinsic state to shareNormal objects
Thread safety requiredShared mutable state needs synchronization, adding complexityImmutable objects or thread-local

8. Flyweight vs Singleton vs Object Pool

AspectFlyweightSingletonObject Pool
ObjectsOne per type (many types cached)One per class (only one instance)Fixed pool of reusable objects
CacheHashMap of shared objectsSingle static referenceQueue/list of available objects
StateIntrinsic shared + extrinsic per callAll state in one objectEach object has its own state
Use caseMany similar objects (shapes, sprites)One global instance (config, logger)Expensive objects (DB connections, threads)
JDK exampleString pool, Integer cacheRuntime.getRuntime()Thread pool (ExecutorService)

9. Quick Reference Card

ComponentRoleIn Game ProjectIn Shape Example
Flyweight InterfaceCommon type for shared objectsSpriteShape
Concrete FlyweightShared object with intrinsic stateTreeSprite, RockSpriteCircle, Square
Flyweight FactoryCreates and caches flyweight objectsSpriteFactoryShapeFactory
Extrinsic StatePer-use data passed by clientSceneObject (x, y, scale)Method params (radius, color)
Intrinsic StateShared data stored in flyweighttexture, meshData, baseColorlabel ("circle"/"square")
Structural GOF Pattern Medium

Flyweight Design Pattern in Java

Complete Tutorial | 10 Sections | Full Game Sprite Rendering Project with HLD and LLD | Intrinsic vs Extrinsic State | Shape Drawing Example | String Pool and Integer Cache | Multiple Scenarios | 12 Interview Questions

1. What is Flyweight Pattern?

Definition

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 TypeDescriptionWhere StoredCircle 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 runtimeradius, fillColor, lineStyle
Circle class with intrinsic/extrinsic separation: public class Circle { private String label; // INTRINSIC (sharable data) label = "circle"; public void draw(int radius, String fillColor, String lineStyle) { // EXTRINSIC (non-sharable data, per call) ... } } Cache (HashMap): Calls: +--------+-----------+ obj.draw(10); O (small circle) | "circle"| Circle obj| obj.draw(20); O (medium circle) +--------+-----------+ obj.draw(30); O (large circle) One object, many uses! All 3 use the SAME cached Circle object

Factors before applying Flyweight

  1. The number of objects to be created by application should be huge
  2. The object creation is heavy on memory and it can be time-consuming too
  3. 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

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.
    }
}
Circle:: constructor Circle:: constructor Circle:: constructor ... (1000 times!) Square:: constructor Square:: constructor ... (1000 times!) Total: 2000 constructor calls, 2000 objects in memory

3. The Solution: Flyweight Factory with Cache

Solution

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!
    }
}
Circle:: 0-param constructor circle object created and cached drawing Circle with radius:0 having fillColor:red and line Style:dotted drawing Circle with radius:1 having fillColor:red and line Style:dotted ... (998 more draws, NO more constructor calls) Square:: 0-param constructor square object created and cached drawing Square with side length:10 having fillColor:green and line Style:solid ... (999 more draws, NO more constructor calls) Total: 2 constructor calls, 2 objects in memory (instead of 2000)
Cache (HashMap<String, Shape>): +----------+--------------------+ | Key | Value | +----------+--------------------+ | "circle" | Circle obj (one) | <-- serves ALL 1000 circle draws | "square" | Square obj (one) | <-- serves ALL 1000 square draws +----------+--------------------+ Memory saved: 1998 objects x ~100 bytes each = ~200 KB saved

4. Flyweight in Spring Framework

Spring and Flyweight

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

ExampleIntrinsic (cached)Extrinsic (per use)
String PoolString literal value. "hello" is created once in the pool.Reference variable name
Integer.valueOf()Integer objects for values -128 to 127 are cachedVariable assignment context
Boolean.TRUE / FALSETwo Boolean objects cachedUsage context
Character.valueOf()Characters 0-127 cachedUsage context
Spring Singleton BeansBean instance in IOC containerMethod 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

Business Scenario

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)

+---------------------------------------------------+ | GAME LOOP | | GameEngine | | - renders forest scene with 10,000 sprites | | - for each sprite: get from factory, draw at pos| +----------------------------+----------------------+ | v +---------------------------------------------------+ | FLYWEIGHT FACTORY | | SpriteFactory | | - cache: HashMap<String, Sprite> | | - getSprite("oak_tree"): Sprite | | - getSprite("pine_tree"): Sprite | | - returns cached instance or creates new | +---------------------------------------------------+ | v +---------------------------------------------------+ | FLYWEIGHT OBJECTS | | Sprite (interface) | | + render(x, y, scale): void | | | | | +----+----+----+ | | | | | | | | OakTree PineTree Birch Rock Bush | | Sprite Sprite Sprite Sprite Sprite | | (intrinsic: texture, mesh, colour) | | (extrinsic: x, y, scale passed per render) | +---------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | Sprite (interface) | +-------------------------------------------------------+ | + render(x: int, y: int, scale: double): void | | + getType(): String | | + getMemorySize(): int (shows texture size in KB) | +-------------------------------------------------------+ ^ ^ ^ | | | +----------------+ +----------------+ +----------------+ | TreeSprite | | RockSprite | | BushSprite | +----------------+ +----------------+ +----------------+ | - type: String | | - type: String | | - type: String | | - texture: String (intrinsic, heavy) | | - meshData: String (intrinsic, heavy) | | - baseColor: String (intrinsic) | +----------------+ +----------------+ +----------------+ +-------------------------------------------------------+ | SpriteFactory (Flyweight Factory) | +-------------------------------------------------------+ | - cache: Map<String, Sprite> [static] | +-------------------------------------------------------+ | + getSprite(type): Sprite [static] | | + getCacheSize(): int [static] | | + getCacheStats(): String [static] | +-------------------------------------------------------+ +-------------------------------------------------------+ | SceneObject (uses flyweight + extrinsic data) | +-------------------------------------------------------+ | - sprite: Sprite [shared flyweight reference] | | - x: int [extrinsic - position] | | - y: int [extrinsic - position] | | - scale: double [extrinsic - size] | +-------------------------------------------------------+ | + render(): void | +-------------------------------------------------------+

Project Structure

forest-game-engine/ ├── src/ │ └── main/java/com/game/forest/ │ ├── sprite/ │ │ ├── Sprite.java (flyweight interface) │ │ ├── TreeSprite.java (concrete flyweight) │ │ ├── RockSprite.java │ │ └── BushSprite.java │ ├── factory/ │ │ └── SpriteFactory.java (flyweight factory with cache) │ ├── scene/ │ │ └── SceneObject.java (extrinsic state holder) │ ├── engine/ │ │ └── GameEngine.java (renders the scene) │ └── Application.java ├── src/ │ └── test/java/com/game/forest/ │ └── factory/ │ └── SpriteFactoryTest.java └── pom.xml

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

Generating forest with 10000 objects... TreeSprite created: pine_tree (texture: 2048 KB loaded) TreeSprite created: oak_tree (texture: 2048 KB loaded) BushSprite created: green_bush (texture: 512 KB loaded) RockSprite created: small_rock (texture: 1024 KB loaded) TreeSprite created: birch_tree (texture: 2048 KB loaded) RockSprite created: large_rock (texture: 1024 KB loaded) BushSprite created: flower_bush (texture: 512 KB loaded) Rendering first 10 objects: Render pine_tree at (1142,503) scale:1.23 [shared texture:texture_pine_tree_2048x2048px_RGB] Render oak_tree at (887,291) scale:0.85 [shared texture:texture_oak_tree_2048x2048px_RGB] Render green_bush at (342,912) scale:1.67 [shared texture:texture_green_bush_512x512px] Render small_rock at (1567,102) scale:0.55 [shared texture:texture_small_rock_1024x1024px] Render pine_tree at (201,775) scale:1.12 [shared texture:texture_pine_tree_2048x2048px_RGB] Render birch_tree at (1890,445) scale:0.92 [shared texture:texture_birch_tree_2048x2048px_RGB] Render oak_tree at (678,623) scale:1.45 [shared texture:texture_oak_tree_2048x2048px_RGB] Render large_rock at (1234,88) scale:0.71 [shared texture:texture_large_rock_1024x1024px] Render flower_bush at (456,999) scale:1.88 [shared texture:texture_flower_bush_512x512px] Render pine_tree at (890,345) scale:1.05 [shared texture:texture_pine_tree_2048x2048px_RGB] === Performance Stats === Total scene objects: 10000 Unique sprites created: 7 Cache: 7 sprites | Hits: 9993 | Misses: 7 | Memory: 9216 KB Memory WITHOUT Flyweight: 15000000 KB (14648 MB) Memory WITH Flyweight: 10500 KB (10 MB) Memory saved: 99%

Key observations from the project

  1. 7 sprite objects serve 10,000 scene objects. Only 7 constructors called (one per sprite type). 9,993 cache hits. 99% memory saved.
  2. 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.
  3. 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.
  4. 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 new directly.
  5. 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

ScenarioExampleWhy Flyweight
Large number of similar objectsGame sprites, document characters, map tilesShare common data, pass unique data per use
Memory-constrained environmentsMobile apps, embedded systems, IoTReduce object count dramatically
Object creation is expensiveLoading textures, parsing configs, DB connectionsCreate once, reuse many times via cache
Most properties can be extrinsicPosition, size, color passed per callShared object + external context = many virtual objects
Caching and poolingString pool, Integer cache, connection poolJVM and frameworks use Flyweight internally

When NOT to use Flyweight

ScenarioWhy NOTUse Instead
Few objects neededCaching overhead not worth it for 10 objectsDirect creation
Each object is uniqueNo intrinsic state to shareNormal objects
Thread safety requiredShared mutable state needs synchronization, adding complexityImmutable objects or thread-local

8. Flyweight vs Singleton vs Object Pool

AspectFlyweightSingletonObject Pool
ObjectsOne per type (many types cached)One per class (only one instance)Fixed pool of reusable objects
CacheHashMap of shared objectsSingle static referenceQueue/list of available objects
StateIntrinsic shared + extrinsic per callAll state in one objectEach object has its own state
Use caseMany similar objects (shapes, sprites)One global instance (config, logger)Expensive objects (DB connections, threads)
JDK exampleString pool, Integer cacheRuntime.getRuntime()Thread pool (ExecutorService)

9. Quick Reference Card

ComponentRoleIn Game ProjectIn Shape Example
Flyweight InterfaceCommon type for shared objectsSpriteShape
Concrete FlyweightShared object with intrinsic stateTreeSprite, RockSpriteCircle, Square
Flyweight FactoryCreates and caches flyweight objectsSpriteFactoryShapeFactory
Extrinsic StatePer-use data passed by clientSceneObject (x, y, scale)Method params (radius, color)
Intrinsic StateShared data stored in flyweighttexture, meshData, baseColorlabel ("circle"/"square")