Flyweight Pattern in Java – Optimize Memory Usage with Shared Object Instances

Illustration for Flyweight Pattern in Java – Optimize Memory Usage with Shared Object Instances
By Last updated:

Introduction

The Flyweight Pattern is a structural design pattern focused on minimizing memory usage by sharing instances of objects that are expensive to create and are used in large numbers.

Why Flyweight Pattern Matters

Imagine you're developing a text editor that renders millions of characters. If each character object holds formatting info (font, style, color), the memory usage would be massive. The Flyweight Pattern helps by reusing common data across multiple objects.


Core Intent and Participants

  • Intent: Share objects to support a large number of fine-grained objects efficiently.

Participants

  • Flyweight: The interface shared by all flyweight objects.
  • ConcreteFlyweight: Implements the Flyweight interface and stores intrinsic state.
  • FlyweightFactory: Creates and manages flyweight objects.
  • Client: Maintains extrinsic state and references flyweights.

UML Diagram (Text)

+------------------+
|   Flyweight      |
+------------------+
| + operation()    |
+------------------+
        ▲
        |
+----------------------+      +-------------------+
| ConcreteFlyweight    |<-----| FlyweightFactory  |
+----------------------+      +-------------------+
                               | + getFlyweight()  |
+---------+ uses +-----------+
| Client  |----->| Flyweight |
+---------+      +-----------+

Real-World Use Cases

  • Text rendering engines (characters share font/style info)
  • Particle systems in games
  • Caching database connections or images
  • Icons in UI apps (multiple buttons using the same icon)
  • Tree structures in syntax trees

Java Implementation Strategy

Example: Drawing Trees in a Forest

Step 1: Flyweight Interface

public interface TreeType {
    void display(int x, int y);
}

Step 2: Concrete Flyweight

public class ConcreteTreeType implements TreeType {
    private String name;
    private String color;
    private String texture;

    public ConcreteTreeType(String name, String color, String texture) {
        this.name = name;
        this.color = color;
        this.texture = texture;
    }

    public void display(int x, int y) {
        System.out.println("Drawing " + name + " tree at (" + x + ", " + y + ") with color " + color);
    }
}

Step 3: Flyweight Factory

import java.util.HashMap;
import java.util.Map;

public class TreeFactory {
    private static final Map<String, TreeType> treeTypes = new HashMap<>();

    public static TreeType getTreeType(String name, String color, String texture) {
        String key = name + "-" + color + "-" + texture;
        if (!treeTypes.containsKey(key)) {
            treeTypes.put(key, new ConcreteTreeType(name, color, texture));
        }
        return treeTypes.get(key);
    }
}

Step 4: Client Class

public class Tree {
    private int x, y; // extrinsic state
    private TreeType type; // shared intrinsic state

    public Tree(int x, int y, TreeType type) {
        this.x = x;
        this.y = y;
        this.type = type;
    }

    public void draw() {
        type.display(x, y);
    }
}

Step 5: Demo

import java.util.ArrayList;
import java.util.List;

public class Forest {
    public static void main(String[] args) {
        List<Tree> trees = new ArrayList<>();

        TreeType pine = TreeFactory.getTreeType("Pine", "Green", "Rough");
        TreeType oak = TreeFactory.getTreeType("Oak", "Brown", "Smooth");

        trees.add(new Tree(1, 1, pine));
        trees.add(new Tree(2, 3, pine));
        trees.add(new Tree(5, 5, oak));

        for (Tree tree : trees) {
            tree.draw();
        }
    }
}

✅ Only two TreeType objects are created and reused across multiple Tree instances.


Pros and Cons

✅ Pros

  • Saves memory by sharing common objects
  • Reduces object creation overhead
  • Improves performance for large-scale object sets

❌ Cons

  • Makes code more complex due to separation of intrinsic/extrinsic state
  • Less flexibility in storing state within shared objects
  • Debugging shared state issues can be tricky

Anti-Patterns and Misuse

  • Using Flyweight for objects that rarely repeat (no benefit)
  • Mixing intrinsic and extrinsic data inside the Flyweight (breaks pattern)
  • Over-caching in the factory, leading to memory leaks

Flyweight vs Singleton vs Object Pool

Pattern Purpose Sharing Scope Object Lifecycle Control Real-World Use Case
Flyweight Share many instances' data Many-to-many Managed by factory Fonts, icons, text rendering
Singleton Ensure one instance per class Global Single instance Logger, config
Object Pool Reuse a limited pool of objects One-to-few Checkout/checkin model DB connections, threads

Refactoring Legacy Code

Before

new ConcreteTreeType("Pine", "Green", "Rough") // Every time

After (Using Flyweight Factory)

TreeFactory.getTreeType("Pine", "Green", "Rough") // Shared

✅ Reduces memory and improves performance.


Best Practices

  • Separate intrinsic (shared) and extrinsic (per-object) data
  • Use factories for object reuse
  • Combine with lazy loading and caching mechanisms
  • Avoid premature optimization—profile first

Real-World Analogy

Imagine a car rental agency. Instead of giving everyone a new car every time, they maintain a limited set of vehicles (shared objects) and rent them to different users (extrinsic state). This is Flyweight in real life.


Java Version Relevance

  • Java 8+: Use Map.computeIfAbsent() for cleaner factories
  • Java 11+: Enhanced performance helps object caching strategies
  • Java 17+: Use sealed types to restrict Flyweight implementations

Conclusion & Key Takeaways

  • Flyweight is a memory-saving pattern used in large-scale, repetitive object usage.
  • Share intrinsic state and manage unique data separately.
  • Use factory classes to efficiently manage instance reuse.
  • Perfect for UI elements, text processing, games, and more.

FAQ – Flyweight Pattern in Java

1. What is the Flyweight Pattern?

A structural pattern to reduce memory by sharing common parts of objects.

2. When should I use Flyweight?

When your application has thousands or millions of similar objects.

3. How does it differ from Object Pool?

Object Pool manages object lifecycle. Flyweight shares immutable data.

4. Can Flyweight have mutable state?

No. It should only store immutable, shared state.

5. What’s intrinsic vs extrinsic data?

Intrinsic: shared (e.g., font), Extrinsic: unique (e.g., position)

6. How does it help performance?

It minimizes object creation and reduces memory usage.

7. Is it thread-safe?

Flyweights should be immutable to ensure thread safety.

8. Can I use it with Spring Beans?

Yes, with prototype-scope beans managed manually via factory.

9. Does Java use Flyweight internally?

Yes, in Integer.valueOf(), String.intern(), Boolean.TRUE/FALSE

10. Is it suitable for modern Java apps?

Yes, especially in graphics-heavy or high-performance apps.