Flyweight Design Pattern in Java

Learn the Flyweight design pattern in Java with a runnable pen factory example. See what state to share, how the cache key works, and when reuse is worthwhile.

Flyweight Pattern

A drawing tool may use the same brush size for thousands of marks, even though each mark has its own color and text. The Flyweight pattern lets those marks reuse a pen for the shared size and pass the changing details to each drawing call.

Use sharing to support large numbers of fine-grained objects efficiently.

A flyweight stores reusable data, while each operation supplies the details that differ. Changing the color for one draw then does not change a shared pen for another caller. Sharing helps when an application would otherwise keep many copies of the same data.

1. How the Flyweight Pattern Works

Pattern descriptions call shared data intrinsic state. In this example, that is the brush size stored inside a pen. Per-use data is extrinsic state. Here, the color and content arrive as method arguments.

Data Where it lives Why
Brush size Inside the shared pen All callers using that pen agree on its size
Color Argument to a drawing operation Different callers can choose different colors
Content Argument to a drawing operation Each drawing request can contain different text

The choice depends on what we intend to share. Another application could share a pen by both size and color. What matters is that the shared object’s state remains consistent with the key used to retrieve it.

The following participants take part in this interaction.

Participant Responsibility
Flyweight Holds reusable state and accepts the changing details of an operation.
Factory Finds or creates a flyweight for a cache key.
Client Requests a flyweight and supplies details for each operation.

A caller requests a size from the factory. Repeated requests for that size return the same pen; each draw supplies its own changing data.

Generic Flyweight relationship: a client obtains a shared flyweight from a factory and supplies per-use state to its operation

The factory’s key must describe all the state stored in the shared object. If a pen stored both size and color but the factory searched only by size, a request for a new color could return a pen with the wrong color. Our pen stores only size, so size is a complete key.

2. Flyweight Pattern in Action

Our drawing tool uses a pen to print marks. A brush size belongs to the pen, while color and content can differ for each mark. The following types put the generic roles into this example.

Example type Pattern role Responsibility
FlyweightDemo Client Requests pens and supplies color and content to each draw.
PenFactory Factory Keeps one Pen for each BrushSize.
Pen Flyweight Stores its brush size and draws with the supplied details.
BrushSize Cache key Identifies the size of a shared pen.
Concrete Flyweight classes: FlyweightDemo requests pens by BrushSize from PenFactory and calls Pen.draw with color and content

The client asks one factory for a thin pen twice. The factory returns the same Pen both times. Each draw() call provides its own color and content, so neither value needs to stay in the shared object.

3. Flyweight Pattern Implementation

The complete example is in the Flyweight source directory. The excerpts omit the package declaration and show the types that live together in FlyweightDemo.java.

Keep the Shared Pen Immutable

A record is a compact data carrier with an accessor for each component. Our Pen record contains its size and provides a drawing method. Record component fields are final, but a record can still refer to mutable objects. This pen is safe to share because its only component is an enum value, which cannot change.

enum BrushSize {
    THIN, MEDIUM, THICK
}

record Pen(BrushSize size) {
    Pen {
        Objects.requireNonNull(size);
    }

    void draw(String color, String content) {
        System.out.println(size + " " + color + " " + content);
    }
}

We do not add a setColor() method or store the last color in a field. If callers changed shared pen state before every draw, their requests could interfere with each other, especially across threads. The constructor rejects a missing size.

The method prints its arguments instead of drawing on a real canvas. That keeps the example focused on which state is shared and which state belongs to an individual request.

Reuse Pens Through a Factory

PenFactory keeps one pen for each brush size. It returns an existing pen when the requested size is already present.

import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

final class PenFactory {
    private final ConcurrentMap<BrushSize, Pen> pens = new ConcurrentHashMap<>();

    Pen get(BrushSize size) {
        Objects.requireNonNull(size);
        return pens.computeIfAbsent(size, Pen::new);
    }
}

The map belongs to this factory instance. Two different factories can maintain two different sets of pens. Within one factory, computeIfAbsent() coordinates lookup and creation for a missing key. This does not make the whole drawing application thread-safe.

The concurrent map contract describes that operation. We do not need a separate double-checked locking implementation for this cache.

4. Demo

We put the imports first, followed by the types above and this entry point in FlyweightDemo.java.

public final class FlyweightDemo {
    public static void main(String[] args) {
        PenFactory factory = new PenFactory();
        Pen first = factory.get(BrushSize.THIN);
        Pen second = factory.get(BrushSize.THIN);
        Pen thick = factory.get(BrushSize.THICK);

        first.draw("BLUE", "Hello");
        second.draw("RED", "World");

        System.out.println("Same size reuses a pen: " + (first == second));
        System.out.println("Different size reuses a pen: " + (first == thick));

        Pen otherFactoryPen = new PenFactory().get(BrushSize.THIN);
        System.out.println("Separate factories share a pen: " + (first == otherFactoryPen));
    }
}
javac -d target/flyweight-demo -sourcepath src/main/java src/main/java/com/howtodoinjava/patterns/structural/flyweight/FlyweightDemo.java
java -cp target/flyweight-demo com.howtodoinjava.patterns.structural.flyweight.FlyweightDemo
THIN BLUE Hello
THIN RED World
Same size reuses a pen: true
Different size reuses a pen: false
Separate factories share a pen: false

The first two calls use the same thin pen with different colors. The identity comparison with == confirms that first and second refer to the same object. Comparing hash codes would not be enough because different objects can have the same hash code.

The second comparison is false because the thick pen has different shared state. The last is false because the two factories have separate caches. This demonstrates object reuse within one factory; it is not a measured memory benchmark.

5. Decide Whether Sharing Is Worthwhile

Flyweight helps when repeated state is large or repeated many times and can be shared safely. A document renderer might share font or glyph data while keeping each character’s position separately.

Our pen record is deliberately small, so a map may cost more than the objects it saves in a tiny program. In a real application, we measure the allocation and retained-memory costs before adding a sharing layer. The performance guide explains how to compare such changes.

A cache with an unbounded number of keys can also retain too much memory. Brush size has only three values here, but user-supplied keys may need eviction or another lifetime policy.

6. Similar Patterns

The Singleton pattern controls creation of a single instance within its defined scope. Flyweight can provide many shared objects, one for each distinct set of reusable data.

Neither pattern makes mutable state safe automatically. Our factory coordinates pen creation, and each pen contains immutable shared data. A real drawing destination would still need its own concurrency rules.

7. FAQs

Must every flyweight be immutable?

Immutability is the simplest way to keep shared state stable. A mutable flyweight needs rules that prevent one caller from changing what other callers observe. Our Pen stores only its immutable size and receives color and content as arguments.

Must the factory be global?

No. Our PenFactory owns its cache. Two factories return different pens for the same size, as the demo shows. Choose a factory lifetime that matches the callers that should share objects.

8. Conclusion

Flyweight shares stable data and keeps changing request data outside the shared object. In this example, the factory reuses pens by size, while each draw supplies its own color and content. We use the pattern when that separation produces a useful memory benefit without making ownership or lifetime unclear.

Happy Learning!

Leave a Comment

  1. Thanks for this!

    provide a centralized mechanism to control the states of many “virtual” objects.

    Can you explain what you mean by ‘virtual object’?

    • Here virtual object refers to the shared flyweight object. It appears that we are creating a new object in each context, but internally we are sharing the same objects in multiple contexts and places. Virtually we are creating new objects, in reality, we are not.

  2. Hii lokesh we can make extrinsic attribute immutable. In multi threading environment every thread will be sure same extrinsic object they are working with no other threads modifies it.

    Private final String color;

    MediumPen (String color){
    this.color=color;
    }

    Using above approach instead of setter metter for setting extrinsic attribute.

  3. Hi there,
    The issue I am facing is that you need to know a compile-time the name of all the flyweights objects – there are cases that that decision is taken at runtime.
    What would you suggest here? generating and compiling classes at runtime?

  4. hi Lokesh,

    Thanks for your time to create this post.

    Suppose I have created a system using the exact logic you provided.

    Suppose its a multi threaded app,

    Thread1: T1 requested YELLOW pen object and factory returns it successfuly.

    Thread2: T2 requested YELLOW pen object(but T1 has not completed its work) and factory returned the object which is being used by T1.

    My Solution: If I synchronize the below method that should solve my concurrency problems right?

    @Override
    public void draw(String content) {
    System.out.println(“Drawing THICK content in color : ” + color);
    }

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.