Intent

Use sharing to support large numbers of fine-grained objects efficiently. Flyweight separates the intrinsic (shared, immutable) state of an object from its extrinsic (per-instance, contextual) state — and shares the intrinsic part across every client that needs it.

The Problem

You're building the rendering system for a game. The world has forests — say, fifty thousand trees scattered across the map. Each tree has:

  • Mesh geometry (the 3D shape of the trunk and leaves) — about 200 KB.
  • A texture atlas (bark and leaf images) — about 1.5 MB.
  • A material definition (shaders, lighting) — about 50 KB.
  • Plus per-tree data: position (x, y, z), rotation, scale, age.

A naive new Tree(...) for each gives you fifty thousand × 1.75 MB = **85 GB of memory** just for the tree data. You crash before the level loads.

But almost all of that is the same per tree of the same species. The mesh, texture, and material don't differ between two pine trees — only the position and rotation do. The shape of the smell: most of every object's state is identical to its neighbors.

The Solution

Flyweight says: split each tree's state into two parts:

  • Intrinsic state — what's shared and immutable across all trees of the same species: mesh, texture, material. This becomes a single shared object.
  • Extrinsic state — what's per-tree and context-dependent: position, rotation, scale. The client holds this and passes it in when it needs the flyweight.
PHP
// Intrinsic: shared across every Pine in the world.
final class TreeSpecies
{
    public function __construct(
        public readonly Mesh     $mesh,
        public readonly Texture  $texture,
        public readonly Material $material,
    ) {}

    public function render(float $x, float $y, float $z, float $rotation): void
    {
        $renderer->draw($this->mesh, $this->texture, $this->material, $x, $y, $z, $rotation);
    }
}

// Factory ensures only one TreeSpecies exists per species name.
final class TreeSpeciesFactory
{
    /** @var array<string, TreeSpecies> */
    private array $cache = [];

    public function get(string $species): TreeSpecies
    {
        return $this->cache[$species] ??= new TreeSpecies(
            $this->loadMesh($species),
            $this->loadTexture($species),
            $this->loadMaterial($species),
        );
    }
}

The client (the forest) holds 50,000 small structs — each just (x, y, z, rotation, speciesRef). Each struct is maybe 40 bytes. That's 50,000 × 40 = 2 MB of forest data, plus a handful of shared TreeSpecies objects. From 85 GB to 2 MB — same scene, four orders of magnitude less memory.

Real-World Analogy

A library. You wouldn't expect the library to keep one physical copy of Pride and Prejudice per reader — the warehouse would never fit. They keep one or two copies, and lend them to whoever's currently reading. Many readers share the same book, each with their own bookmark slipped into a different page.

The book is the intrinsic, shared state. The bookmark — the per-reader page number, marginalia, dog-eared corners — is the extrinsic state, owned by the reader. The library never confuses them, and the system scales to ten thousand readers without ten thousand copies of every book.

Structure

Flyweight pattern: a TreeSpeciesFactory ensures one TreeSpecies per species; the Forest holds many small TreeInstance structs that hold extrinsic state and a reference to a shared TreeSpecies.
Flyweight: shared intrinsic state in a few flyweight objects; per-instance extrinsic state held by clients.

Three roles you'll see in every Flyweight implementation:

  • Flyweight — the shared object holding intrinsic state. Immutable. Often very heavy. Here: TreeSpecies.
  • Flyweight Factory — manages the cache of flyweights. Ensures only one instance per intrinsic state. Here: TreeSpeciesFactory.
  • Client / Context — holds extrinsic state and a reference to the shared flyweight. Many clients share each flyweight. Here: each TreeInstance in the forest.

The defining property: the client holds the extrinsic state and passes it to the flyweight when calling its methods. The flyweight is stateless about anything that varies per use.

Code Examples

Here's the forest renderer in five languages. Watch how the flyweight (TreeSpecies) holds the heavy assets, the factory caches them, and the per-tree instances stay tiny.

class TreeSpecies {
  // Intrinsic state — shared across every tree of this species.
  constructor(
    readonly mesh: Mesh,
    readonly texture: Texture,
    readonly material: Material,
  ) {}

  render(x: number, y: number, z: number, rotation: number): void {
    renderer.draw(this.mesh, this.texture, this.material, x, y, z, rotation);
  }
}

class TreeSpeciesFactory {
  private cache = new Map<string, TreeSpecies>();

  get(species: string): TreeSpecies {
    let s = this.cache.get(species);
    if (!s) {
      s = new TreeSpecies(loadMesh(species), loadTexture(species), loadMaterial(species));
      this.cache.set(species, s);
    }
    return s;
  }
}

// Each tree instance is tiny — extrinsic state + a reference to the shared flyweight.
class TreeInstance {
  constructor(
    public x: number, public y: number, public z: number,
    public rotation: number,
    private species: TreeSpecies,
  ) {}

  render(): void { this.species.render(this.x, this.y, this.z, this.rotation); }
}

// 50,000 instances, ~3 species — fits comfortably in memory.
const factory = new TreeSpeciesFactory();
const forest: TreeInstance[] = [];
for (const seed of seeds) {
  forest.push(new TreeInstance(seed.x, seed.y, seed.z, seed.rotation, factory.get(seed.species)));
}
class TreeSpecies:
    """Intrinsic state — shared across every tree of this species."""
    def __init__(self, mesh, texture, material):
        self.mesh, self.texture, self.material = mesh, texture, material

    def render(self, x, y, z, rotation):
        renderer.draw(self.mesh, self.texture, self.material, x, y, z, rotation)

class TreeSpeciesFactory:
    def __init__(self):
        self._cache = {}

    def get(self, species):
        if species not in self._cache:
            self._cache[species] = TreeSpecies(
                load_mesh(species), load_texture(species), load_material(species),
            )
        return self._cache[species]

class TreeInstance:
    __slots__ = ("x", "y", "z", "rotation", "species")
    def __init__(self, x, y, z, rotation, species):
        self.x, self.y, self.z, self.rotation, self.species = x, y, z, rotation, species
    def render(self):
        self.species.render(self.x, self.y, self.z, self.rotation)

factory = TreeSpeciesFactory()
forest = [TreeInstance(s.x, s.y, s.z, s.rotation, factory.get(s.species)) for s in seeds]
public final class TreeSpecies {
    public final Mesh     mesh;
    public final Texture  texture;
    public final Material material;

    public TreeSpecies(Mesh mesh, Texture texture, Material material) {
        this.mesh = mesh;
        this.texture = texture;
        this.material = material;
    }

    public void render(double x, double y, double z, double rotation) {
        Renderer.draw(mesh, texture, material, x, y, z, rotation);
    }
}

public final class TreeSpeciesFactory {
    private final Map<String, TreeSpecies> cache = new ConcurrentHashMap<>();

    public TreeSpecies get(String species) {
        return cache.computeIfAbsent(species, name ->
            new TreeSpecies(loadMesh(name), loadTexture(name), loadMaterial(name)));
    }
}

public final class TreeInstance {
    public final double x, y, z, rotation;
    private final TreeSpecies species;

    public TreeInstance(double x, double y, double z, double rotation, TreeSpecies species) {
        this.x = x; this.y = y; this.z = z; this.rotation = rotation;
        this.species = species;
    }

    public void render() { species.render(x, y, z, rotation); }
}
<?php

namespace App\Forest;

final class TreeSpecies
{
    public function __construct(
        public readonly Mesh     $mesh,
        public readonly Texture  $texture,
        public readonly Material $material,
    ) {}

    public function render(float $x, float $y, float $z, float $rotation): void
    {
        Renderer::draw($this->mesh, $this->texture, $this->material, $x, $y, $z, $rotation);
    }
}

final class TreeSpeciesFactory
{
    /** @var array<string, TreeSpecies> */
    private array $cache = [];

    public function get(string $species): TreeSpecies
    {
        return $this->cache[$species] ??= new TreeSpecies(
            loadMesh($species),
            loadTexture($species),
            loadMaterial($species),
        );
    }
}

final class TreeInstance
{
    public function __construct(
        public float $x, public float $y, public float $z,
        public float $rotation,
        private TreeSpecies $species,
    ) {}

    public function render(): void
    {
        $this->species->render($this->x, $this->y, $this->z, $this->rotation);
    }
}
package forest

import "sync"

type TreeSpecies struct {
    Mesh     Mesh
    Texture  Texture
    Material Material
}

func (s *TreeSpecies) Render(x, y, z, rotation float64) {
    Renderer.Draw(s.Mesh, s.Texture, s.Material, x, y, z, rotation)
}

type TreeSpeciesFactory struct {
    mu    sync.Mutex
    cache map[string]*TreeSpecies
}

func NewTreeSpeciesFactory() *TreeSpeciesFactory {
    return &TreeSpeciesFactory{cache: map[string]*TreeSpecies{}}
}

func (f *TreeSpeciesFactory) Get(species string) *TreeSpecies {
    f.mu.Lock()
    defer f.mu.Unlock()

    if s, ok := f.cache[species]; ok {
        return s
    }
    s := &TreeSpecies{Mesh: loadMesh(species), Texture: loadTexture(species), Material: loadMaterial(species)}
    f.cache[species] = s
    return s
}

type TreeInstance struct {
    X, Y, Z, Rotation float64
    Species           *TreeSpecies
}

func (t *TreeInstance) Render() { t.Species.Render(t.X, t.Y, t.Z, t.Rotation) }

The Java version uses ConcurrentHashMap.computeIfAbsent for thread-safe lazy creation; the Go version uses a mutex for the same reason. In multi-threaded Flyweight implementations, the factory is the synchronisation point — get it wrong and you'll create duplicate flyweights under load, defeating the whole point.

When to Use It

Reach for Flyweight when you can answer "yes" to all of these:

  • You have a very large number of objects — thousands or millions, not dozens.
  • Most of each object's state is shared with many of its neighbors.
  • Memory pressure is real and measured. Don't optimize without profiling.
  • The shared state is naturally immutable. If "the same" tree species can mutate, sharing it across instances will cause subtle, far-away bugs.
  • The extrinsic state is genuinely small — a few coordinates, a status flag — so the per-instance footprint stays low.

If your object count is in the hundreds, this pattern is over-engineering. If your shared state is tiny, the savings won't justify the indirection. The pattern earns its keep at scale and only at scale.

Pros and Cons

Pros

  • Dramatic memory savings when scale is real — orders of magnitude, not percentages.
  • Centralizes management of expensive shared resources in one factory.
  • Makes the "shared vs per-instance" distinction explicit in the type system, which often clarifies the design itself.

Cons

  • The intrinsic / extrinsic split is a real design decision. Get the line wrong and you either don't save much, or you push too much state out to the client and make it awkward.
  • CPU cost can go up. Computing or fetching extrinsic state at every render is more work than reading a field directly. Memory savings sometimes come at a small CPU price.
  • Thread safety is a foot-gun. The factory is shared. Two threads asking for "Pine" simultaneously can both create a fresh TreeSpecies if you don't guard the cache.
  • Debugging is harder. "Why is this tree drawing wrong?" might be a bug in any client passing extrinsic state — not in the flyweight itself.
  • Resource cleanup gets tricky. If a TreeSpecies holds a GPU texture handle, you can't free it until every TreeInstance referencing it is gone.

Pro Tips

  • Profile before optimizing. Flyweight's complexity is only worth it when you've measured the memory problem. Don't apply it preemptively.
  • Default to immutability for the flyweight. A shared mutable flyweight is a debugging nightmare. Make the intrinsic state readonly/final/const/frozen and enforce it.
  • The factory is part of the pattern, not a bolt-on. Without a factory enforcing single-instance-per-key, you've defeated the sharing.
  • Use weak references for caching when appropriate. A WeakValueMap (or equivalent) lets the factory release flyweights nobody references anymore, freeing the underlying resources.
  • Pass extrinsic state as parameters, not by stuffing it back into the flyweight. The whole point is that the flyweight doesn't know its callers. Stash extrinsic state in the client.

Relations with Other Patterns

  • Singleton is often how the Flyweight Factory itself is exposed — there's only one factory per process, holding the cache.
  • Composite trees of fine-grained nodes are the classic case for Flyweight — many leaves can share intrinsic state. Glyphs in a text editor; cells in a spreadsheet; blocks in a Minecraft world.
  • Factory Method is the machinery of the Flyweight Factory — get(key) either returns the cached flyweight or constructs a new one and caches it.
  • Facade sometimes hides the Flyweight machinery from clients — they call a clean façade method and don't see the cache or the intrinsic/extrinsic split.
  • State can be stored as a Flyweight when the same state objects are used across many different Contexts.

Final Tips

The Flyweight pattern is on the advanced end of the catalog — useful but rarely needed, and usually wrong when reached for prematurely. The cleanest applications I've seen weren't in application code at all — they were in framework code (text renderers, scene graphs, parsers) where the number of fine-grained objects genuinely was in the millions and where memory was a measured constraint.

Reach for Flyweight when you have a real scale problem, real shared state, and a real measurement showing memory is the bottleneck. Skip it when you don't — the indirection cost is paid every day, and the memory savings only matter when memory pressure is the bottleneck. Most code in the world is not.