SwadhyAI Notes

All Low-Level Design notes

Structural pattern · save memory by sharing

Flyweight

is a structural pattern that reduces memory use when you have many similar objects. Split each object into two parts: a heavy part that is the same for everyone (and can be shared), and a light part that is unique to each object. In a forest simulation, share one pine texture; keep each tree's x and y separately.

Why it matters: memory without sharing vs with sharing

Suppose one tree texture is about 2 MB, and you place 1,000,000 trees. Copying the texture into every Tree object needs roughly 2 TB — the process will run out of memory. With Flyweight you keep three shared TreeType objects (one per tree kind) plus a million small records that only store coordinates and a pointer to the shared type. That fits in normal memory.

Naive: texture copied on every Tree~2 TB (out of memory)
Flyweight: a few TreeTypes + 1e6 coordinatesa few MB + small structs

In an interview, say what would be duplicated, then show how you stop duplicating it.

Core question

Across a million objects, what data is identical — and can that identical part be shared safely?

Two kinds of state

— shared and immutable

  • Name, texture, default color
  • Glyph metrics for the letter “A” in a font
  • Tile art for “grass”
  • Must be immutable. Create and reuse through a factory that interns by key.

— unique and cheap

  • x, y on the map
  • Selected, health, season tint
  • Where this letter appears on the page
  • Lives on Tree, or is passed into draw(). Never store this on the shared flyweight.

Java sketch: share TreeType, keep position on Tree

A factory TreeType instances. stays on Tree. Client code iterates trees, not shared types.

java
final class TreeType {
  private final String name;
  private final int width;
  private final int height;
  private final byte[] texture; // heavy, shared — never alias caller bytes

  TreeType(String name, int width, int height, byte[] texture) {
    this.name = name;
    this.width = width;
    this.height = height;
    this.texture = texture.clone(); // defensive copy on construct
  }

  byte[] texture() { return texture.clone(); } // do not expose the shared array

  void draw(int x, int y) { /* paint texture at x, y */ }
}

record TreeTypeKey(String name, int width, int height, int textureHash) {}

class TreeTypeFactory {
  private final Map<TreeTypeKey, TreeType> types = new HashMap<>();

  TreeType of(String name, int width, int height, byte[] texture) {
    TreeTypeKey key = new TreeTypeKey(name, width, height, Arrays.hashCode(texture));
    return types.computeIfAbsent(key, k -> new TreeType(name, width, height, texture));
  }
}

class Tree {
  private final int x;
  private final int y;
  private final TreeType type;

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

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

Same idea in other domains

Text editor

Do not create 40,000 separate letter-A objects that each carry a full font table. Keep one Glyph for “A” and pass page coordinates when drawing each occurrence.

Game map

Keep a few tile types and a large grid of references (plus optional overlays). Do not clone the grass bitmap for every cell.

Particles

Share one spark mesh/shader. Store thousands of light records with position, velocity, and age. Prototype clones a full object; Flyweight shares the expensive mesh.

Thread safety: shared data must be immutable

Many threads may call type.draw(x, y) on the same TreeType. That is safe only if name and texture never change, and callers cannot mutate the shared byte[] (clone on construct and when exposing). For a seasonal recolor, pass a tint into draw, or store it on Tree. A mutable shared object creates data races. Pair this pattern with immutability — not with a synchronized setter on TreeType.

How Flyweight differs from related ideas

vs Prototype

Prototype clones a ready object — you still pay for a copy. Flyweight avoids copying the expensive shared middle. You may still clone a Tree that points at a shared TreeType.

vs Cache

Ticket T-1042 is unique. Caching it is lookup by identity. Flyweight is for many similar things that can share the unchanging middle.

vs Factory

The intern map looks like a factory method. The pattern is the shared immutable state, not the of() method itself. A factory that always creates a unique Ticket is not Flyweight.

How to implement it (four steps)

  1. 01

    Split the state

    Intrinsic = shared and frozen. Extrinsic = unique, stored on the light object or passed in at use time.

  2. 02

    Make the shared part immutable

    If one tree needs an autumn tint, store the tint on that Tree (or pass it into draw). Do not mutate TreeType.

  3. 03

    Intern through a factory

    Map from full intrinsic key → flyweight. Same name + size + texture bytes → same instance.

  4. 04

    Keep identity on the light object

    Tree holds x, y, and a TreeType. Clients iterate trees, not types.

Common mistakes

Mutating the shared object

Calling treeType.setSeason(AUTUMN) would change every tree that shares that type. Intrinsic state must stay immutable. Per-tree variation belongs on Tree, or as an argument to draw().

Putting x and y on the flyweight

If TreeType stores a position, you cannot share it across trees. Keep position (extrinsic data) outside the shared object.

Using Flyweight for unique entities

Orders, tickets, and users have unique identity and lifecycle. Sharing them is a bug. Share glyphs and textures — not ticket T-1042.

How to explain this in an interview

“A million trees can share a few TreeType objects. The texture is intrinsic and immutable — I clone the bytes when constructing and when exposing them. I intern by name, dimensions, and texture hash, not by name alone. Coordinates are extrinsic and live on Tree. This is not a cache of unique tickets.”
  1. Estimate memory with and without sharing for 1,000,000 trees and a 2 MB texture. What must stay immutable?
  2. Why is immutability required on the shared part?

Takeaway

Flyweight splits heavy shared immutable state from cheap unique state. Intern and share the first. Keep identity and position on the second. Never mutate shared state to “update one tree” — that would change every object that shares it.