Skip to main content
CleanCodeMastery

Flyweight Pattern: One Jersey Design, a Whole Team of Players

Learn the Flyweight pattern with a cricket jersey story. Share the heavy common data between thousands of objects and save huge amounts of memory.

23 min read Updated June 11, 2026beginner
design-patternsstructural-patternsflyweightmemory-optimizationcachingtypescriptcsharp

๐Ÿ One jersey design, eleven players, lakhs of fans

Meet Kiran, who runs a small cricket fan shop in Pune. World Cup season is coming, and his best seller is the Indian team jersey with custom name printing. His younger sister Anjali is a CSE student who helps him with the shop's billing software during holidays โ€” and she is about to find a serious bug in it.

First, think about how the jersey itself comes to exist. The design team works for months on it: the exact shade of blue, the tricolour strokes, the sponsor logos, the fabric pattern. That design is heavy work, and it is done once.

Now the team needs jerseys for Kohli, Bumrah, Gill, and the rest. Does the factory design a brand new jersey from zero for every player? Of course not. Every player's jersey uses the same shared design. Only two small things change per player: the name on the back and the number.

Go one step further, to Kiran's shop. Lakhs of fans buy the same jersey, each printing their own name on the back. One design; lakhs of jerseys. The factory stores one master design file, not lakhs of copies of it.

Split a jersey into its two parts:

  • The shared part โ€” colour, pattern, logos, fabric. Same for everyone. Heavy to create. Stored once.
  • The unique part โ€” name and number. Different for everyone. Tiny. Stored per person.

Now imagine if the factory was foolish and saved a full copy of the entire multi-GB design file inside every jersey record. Ten lakh jerseys, ten lakh copies of the same design. Storage would explode for no reason at all.

Anjali's discovery: Kiran's billing software was doing exactly this foolish thing with objects in memory โ€” every sold-jersey record carried its own full copy of the design images. The Flyweight pattern is the factory's common sense applied to code: store the heavy shared part once, keep only the tiny unique part per object, and let everyone point to the shared part.

Figure 1: A fan buying a custom jersey at Kiran's shop

Notice the last section. The record saved per sale is tiny โ€” a name, a number, and a pointer. The heavy design is fetched, never copied. Hold that picture; the whole pattern is in it.

What is the Flyweight pattern?

Flyweight is a structural design pattern that lets you fit far more objects into the same amount of RAM. It works by splitting an object's data into two groups and treating them differently:

  • Intrinsic state โ€” the data that is the same across many objects and never changes (the jersey design). This goes inside a shared object called the flyweight, stored exactly once.
  • Extrinsic state โ€” the data that is unique to each object (player name, number). This stays outside the flyweight, kept in a small context object or passed into methods as parameters.

A flyweight factory hands out flyweights. When two pieces of code ask for "the India ODI jersey design", the factory gives both of them the same object from its cache. That is why the pattern's other name is Cache.

The two kinds of state, side by side โ€” this table is the entire pattern:

Question to askIntrinsic stateExtrinsic state
Jersey exampleColours, logos, fabric patternFan name, number on the back
Same across objects?Yes โ€” identical for lakhsNo โ€” unique per object
Can it change?Never โ€” frozen at creationFreely, per context
Where does it live?Inside the shared flyweightIn the context, or passed as parameters
How big is it?Heavy (KBs to MBs)Tiny (a few bytes)
How many copies in RAM?One per distinct designOne per object โ€” unavoidable

One rule is absolutely non-negotiable: the flyweight must be immutable (read-only after creation). One object is shared by thousands of contexts; if anyone could scribble on it, everyone's data would change at once.

๐Ÿ’ก

Memory trick for the names: intrinsic = inside = identical (shared, stored in the flyweight). Extrinsic = external = exclusive (unique, kept outside and passed in). If you can label each field of your class with one of these two words, you have already done the hardest part of the pattern.

College corner โ€” the formal split: intrinsic state is context-free information: it depends only on what the thing is ("India ODI jersey"), never on where or how it is used. Extrinsic state is context-dependent: it changes per use site (whose back, which number, which shelf). The pattern works because context-free data can be safely aliased โ€” many references to one immutable object are indistinguishable from many copies, except in the memory bill. This is the same insight behind hash consing in compilers and functional languages, where identical immutable subtrees are stored once and compared by pointer. The moment data becomes mutable or context-dependent, aliasing becomes visible (one writer changes everyone's view) and the trick collapses. That is why immutability is not a style preference here โ€” it is the load-bearing wall.

The problem it solves

Here is the shape of the bug Anjali found. The shop's system keeps an object for every jersey ever sold, and the naive class stores everything in every object:

// The naive way: every jersey carries the FULL design
class Jersey {
  constructor(
    public fanName: string,        // unique  โ€” fine
    public num: number,            // unique  โ€” fine
    public teamName: string,       // same for lakhs of jerseys
    public colorScheme: string,    // same for lakhs of jerseys
    public logoImage: Uint8Array,  // ~200 KB โ€” same for lakhs of jerseys!
    public fabricPattern: Uint8Array, // ~300 KB โ€” same again!
  ) {}
}

Sell 10 lakh (1,000,000) jerseys of one design. The name and number are genuinely different in each object โ€” that costs almost nothing. But the half-megabyte of logo and fabric data is identical in every single object. The maths is brutal:

  • 1,000,000 jerseys ร— ~500 KB of duplicated design data โ‰ˆ 500 GB of RAM
  • Useful unique data: 1,000,000 ร— a few dozen bytes โ‰ˆ a few MB

The program crashes with an out-of-memory error, and 99.99% of what filled the memory was the same bitmap stored a million times. Nothing is wrong with the logic โ€” only with the duplication.

Figure 2: Duplicated heavy data vs one shared flyweight

After the fix, memory cost is roughly (10 lakh tiny contexts) + (1 design ร— 500 KB). From 500 GB down to a few MB. Same forest of objects, a fraction of the RAM.

Watch what the two roads look like as the shop grows. The naive line climbs in a straight, deadly slope; the flyweight line barely moves, because only the tiny per-jersey part grows:

Figure 3: The memory saving curve as jersey count grows

At 100k jerseys the naive approach wants about 50 GB just for duplicated design bytes; the flyweight approach needs single-digit MB โ€” the one design plus 100k tiny records. The gap widens forever as you scale. That widening gap is the whole reason this pattern exists.

College corner โ€” do the memory math before you build: the general formula is simple. Naive cost โ‰ˆ N ร— (unique bytes + shared bytes). Flyweight cost โ‰ˆ N ร— (unique bytes + one pointer) + D ร— shared bytes, where N is the object count and D is the number of distinct designs. The saving factor is roughly (shared bytes) รท (unique bytes + 8) when D is small. For our jerseys: shared โ‰ˆ 500,000 bytes, unique โ‰ˆ 50 bytes, so the saving is around 10,000ร—. But flip the numbers โ€” shared 50 bytes, unique 500 KB โ€” and the pattern saves almost nothing while adding a factory, a cache, and indirection on every call. Run this two-line calculation before writing any flyweight code; it tells you honestly whether the pattern will pay.

โš™๏ธ How it works, step by step

  1. Sort the fields into two buckets. For each field of the heavy class ask: "Is this identical across many objects, and constant?" If yes โ†’ intrinsic. If it differs per object โ†’ extrinsic. (Design = intrinsic; name and number = extrinsic.)
  2. Build the flyweight class with intrinsic fields only. Set them in the constructor and never expose any setter. The flyweight is immutable.
  3. Change methods to accept extrinsic state as parameters. A method that used to read this.fanName now takes fanName as an argument: print(fanName, num).
  4. Build a flyweight factory with a cache. It keeps a map keyed by intrinsic state ("india-odi-2026" โ†’ flyweight). On request, it returns the cached object if it exists, otherwise creates it once and caches it.
  5. Never call the flyweight constructor directly from client code. Only the factory does that โ€” this is what guarantees actual sharing.
  6. Make a small context class holding the extrinsic fields plus a reference to the shared flyweight. Many contexts, few flyweights.
Figure 4: Class structure of the Flyweight pattern

Read it as a story: the Shop (client) asks the DesignFactory for a design. The factory returns a shared, immutable JerseyDesign. The shop then creates a tiny Jersey context holding only the fan's name, number, and a pointer to that shared design.

The factory's cache has a tiny life cycle of its own, and it is worth drawing, because every flyweight bug lives somewhere in this diagram:

Figure 5: Life of one design inside the factory cache

The expensive Creating state is visited once per distinct design, no matter how many thousand requests arrive. Every later request takes the cheap Cached โ†’ Cached loop. If your logs show Creating firing again and again for the same design, someone is bypassing the factory.

Real-life code example

Full jersey shop in TypeScript. Note the counter at the end โ€” it proves the memory saving by printing how many heavy objects actually exist.

// ----- The Flyweight: intrinsic (shared, immutable) state only -----
class JerseyDesign {
  // 'readonly' everywhere โ€” a flyweight must never change.
  constructor(
    public readonly team: string,
    public readonly kit: string,          // "ODI" | "Test" | "T20"
    public readonly colorScheme: string,
    public readonly designData: string,   // imagine ~500 KB of image bytes
  ) {
    JerseyDesign.created++;               // count heavy objects made
    console.log(`  (heavy work) Designing ${team} ${kit} jersey...`);
  }
 
  static created = 0;
 
  // Extrinsic state (fanName, num) is PASSED IN, never stored here.
  print(fanName: string, num: number): void {
    console.log(
      `${this.team} ${this.kit} [${this.colorScheme}] -> ${fanName} #${num}`
    );
  }
}
 
// ----- The Flyweight Factory: one cache for the whole shop -----
class DesignFactory {
  private cache = new Map<string, JerseyDesign>();
 
  getDesign(team: string, kit: string, colors: string): JerseyDesign {
    const key = `${team}-${kit}`;
    if (!this.cache.has(key)) {
      // Heavy creation happens only ONCE per distinct design.
      this.cache.set(
        key,
        new JerseyDesign(team, kit, colors, "<500KB of image data>")
      );
    }
    return this.cache.get(key)!;          // everyone shares THIS object
  }
 
  get designCount(): number {
    return this.cache.size;
  }
}
 
// ----- The Context: tiny, unique, one per sold jersey -----
class Jersey {
  constructor(
    private fanName: string,              // extrinsic
    private num: number,                  // extrinsic
    private design: JerseyDesign,         // reference to SHARED flyweight
  ) {}
 
  print(): void {
    this.design.print(this.fanName, this.num);
  }
}
 
// ----- The client: Kiran's shop sells thousands of jerseys -----
const factory = new DesignFactory();
const sold: Jersey[] = [];
 
const fans = ["Aarav", "Diya", "Kabir", "Meera", "Ishaan", "Anaya"];
 
// Sell 10,000 jerseys across only 3 designs.
for (let i = 0; i < 10_000; i++) {
  const fan = fans[i % fans.length] + "-" + i;
  const kit = ["ODI", "Test", "T20"][i % 3];
  const design = factory.getDesign("India", kit, "Blue/Tricolour");
  sold.push(new Jersey(fan, (i % 99) + 1, design));
}
 
// Print a small sample.
sold[0].print();
sold[1].print();
sold[2].print();
 
// ----- The PROOF: shared objects vs total objects -----
console.log("-".repeat(50));
console.log(`Total jerseys sold (contexts) : ${sold.length}`);
console.log(`Heavy design objects created  : ${JerseyDesign.created}`);
console.log(`Designs in factory cache      : ${factory.designCount}`);
console.log(
  `Memory for designs: ~${JerseyDesign.created * 500} KB instead of ~${
    sold.length * 500
  } KB`
);

Output:

  (heavy work) Designing India ODI jersey...
  (heavy work) Designing India Test jersey...
  (heavy work) Designing India T20 jersey...
India ODI [Blue/Tricolour] -> Aarav-0 #1
India Test [Blue/Tricolour] -> Diya-1 #2
India T20 [Blue/Tricolour] -> Kabir-2 #3
--------------------------------------------------
Total jerseys sold (contexts) : 10000
Heavy design objects created  : 3
Designs in factory cache      : 3
Memory for designs: ~1500 KB instead of ~5000000 KB

Read the proof lines slowly. We created 10,000 jersey objects, but the heavy "designing" message printed only 3 times. Ten thousand contexts share three flyweights. The design memory dropped from about 5 GB to about 1.5 MB โ€” a saving of more than 99.9%. That is the whole pattern in two numbers: many contexts, few flyweights.

Here is the factory conversation behind those numbers โ€” the first request pays, every later request rides free:

Figure 6: First request creates; every later request shares

And here is where the memory actually sits after the fix. Almost the entire bill is now the tiny per-jersey data โ€” the heavy design has shrunk to a sliver because it exists once:

Figure 7: Memory split after applying Flyweight

Compare this pie with the naive world, where the "shared designs" slice would be 99.9% of a chart ten thousand times bigger. A healthy flyweight system always looks like this: the unavoidable unique data dominates, and the shared part is a rounding error.

The same idea in C#

// Flyweight: immutable shared state. A record is perfect โ€” immutable by default.
public record JerseyDesign(string Team, string Kit, string Colors)
{
    public static int Created = 0;
    public void Print(string fanName, int num) =>
        Console.WriteLine($"{Team} {Kit} -> {fanName} #{num}");
}
 
// Factory with a cache
public class DesignFactory
{
    private readonly Dictionary<string, JerseyDesign> _cache = new();
 
    public JerseyDesign Get(string team, string kit, string colors)
    {
        var key = $"{team}-{kit}";
        if (!_cache.TryGetValue(key, out var design))
        {
            design = new JerseyDesign(team, kit, colors);
            JerseyDesign.Created++;
            _cache[key] = design;
        }
        return design;
    }
}
 
// Context: tiny per-jersey data + shared reference
public class Jersey
{
    private readonly string _fan; private readonly int _num;
    private readonly JerseyDesign _design;
    public Jersey(string fan, int num, JerseyDesign design)
        => (_fan, _num, _design) = (fan, num, design);
    public void Print() => _design.Print(_fan, _num);
}
 
// Client
var factory = new DesignFactory();
var sold = new List<Jersey>();
for (int i = 0; i < 10_000; i++)
{
    var kit = new[] { "ODI", "Test", "T20" }[i % 3];
    sold.Add(new Jersey($"Fan-{i}", i % 99 + 1, factory.Get("India", kit, "Blue")));
}
Console.WriteLine($"Jerseys: {sold.Count}, Designs created: {JerseyDesign.Created}");
// Output: Jerseys: 10000, Designs created: 3

Fun fact: C# itself flyweights for you. Identical string literals in your code are interned โ€” stored once and shared โ€” and ReferenceEquals("blue", "blue") returns true.

๐Ÿ“š The textbook printing press, in Python

Anjali's college gave her a perfect second example. A state board prints lakhs of copies of one Class 7 Science textbook. Does the press store the full 300-page layout once per printed copy? Never. One master plate; lakhs of copies; each copy differs only by its serial number and the school it ships to. Same pattern, new clothes:

class BookDesign:
    """Flyweight: the master plate. Intrinsic, immutable, made once."""
    created = 0
 
    def __init__(self, title, subject, page_layout):
        BookDesign.created += 1
        print(f"  (heavy work) Typesetting master plate: {title}")
        self._title = title            # no setters anywhere โ€”
        self._subject = subject        # the plate is frozen
        self._page_layout = page_layout
 
    def print_copy(self, serial, school):
        # Extrinsic state arrives as parameters, never stored here.
        print(f"{self._title} | copy #{serial} -> {school}")
 
class PressFactory:
    """Factory: one cache of master plates for the whole press."""
    def __init__(self):
        self._plates = {}
 
    def get_design(self, title, subject, layout):
        if title not in self._plates:
            self._plates[title] = BookDesign(title, subject, layout)
        return self._plates[title]
 
class PrintedCopy:
    """Context: tiny, unique, one per physical book."""
    def __init__(self, serial, school, design):
        self.serial = serial           # extrinsic
        self.school = school           # extrinsic
        self.design = design           # pointer to the SHARED plate
 
    def print_label(self):
        self.design.print_copy(self.serial, self.school)
 
# The press run: 50,000 copies across 4 textbook designs.
press = PressFactory()
titles = ["Science 7", "Maths 7", "English 7", "History 7"]
copies = [
    PrintedCopy(i, f"School-{i % 900}",
                press.get_design(titles[i % 4], "Class 7", "<300-page layout>"))
    for i in range(50_000)
]
 
copies[0].print_label()
copies[1].print_label()
print("-" * 50)
print(f"Copies printed (contexts) : {len(copies)}")
print(f"Master plates made        : {BookDesign.created}")   # -> 4

Fifty thousand contexts, four flyweights. The proof lines are the same two numbers as the jersey shop โ€” they always are. By the way, Python flyweights small integers for you: every -5 to 256 in your whole program is the same shared object, which you can check with a = 100; b = 100; print(a is b).

Where you see it in real software

Flyweight hides inside languages and engines you already use.

  • String interning. Java, C#, and Python keep one shared copy of identical string literals. Write "team india" in a hundred places; the runtime stores it once and every variable points at the same immutable object. Immutability of strings is exactly what makes this safe โ€” the flyweight rule in action.
  • Java's Integer cache. Integer.valueOf(n) returns pre-made shared objects for every value from -128 to 127. Boolean.TRUE and Boolean.FALSE are two flyweights for the whole JVM. BigDecimal.ZERO, ONE, and TEN are the same trick.
  • Game sprites and meshes. A forest of one million trees in a game keeps just a handful of tree meshes and textures in GPU memory and draws them at a million different positions. The position is extrinsic; the mesh is intrinsic. The same idea powers bullets, particles, and monster types in shooters โ€” the Game Programming Patterns book has a beautiful chapter on this.
  • Text editors and browsers. A page holds lakhs of characters, but the glyph shape and font data for the letter "a" exists once; each character on screen stores only its position and a reference to the shared glyph.
  • Open-source reference. The java-design-patterns flyweight example models an alchemist shop where potion objects are shared from a cache instead of brewed again.

๐Ÿงญ When to use it and when not to

Flyweight is a specialised tool. Be honest with this checklist โ€” all the โœ… conditions should hold.

SituationUse Flyweight?
You must keep a huge number of objects alive (lakhs+) and RAM is hurtingโœ… Yes
A large chunk of each object's data is identical across objectsโœ… Yes โ€” that chunk becomes the flyweight
The repeated data is immutable, or can be made immutableโœ… Yes โ€” sharing is safe
Objects stay useful after the shared part is pulled outโœ… Yes
You have a few hundred objectsโŒ No โ€” savings are pennies, complexity is real
Every object's data is genuinely uniqueโŒ No โ€” there is nothing to share
The "shared" data must change per object sometimesโŒ No โ€” mutation breaks sharing; rethink the split
CPU is your bottleneck, not memoryโŒ Careful โ€” Flyweight trades RAM for CPU (extrinsic data is passed/recomputed on every call)

The same checklist as a map. Plot your own case before you write a single class:

Figure 8: Should you use Flyweight here?

Top-right โ€” huge counts, mostly shared data โ€” is flyweight country. Bottom-right is the trap students fall into: lots of objects but nothing meaningfully shared, where the pattern adds machinery and saves nothing.

Common mistakes students make

โš ๏ธ

Mistake 1: Mutating the flyweight. One line like design.colorScheme = "red" silently repaints every jersey in the shop. Mark intrinsic fields readonly, give no setters, and treat any "let me just tweak the shared object" thought as a bug.

Mistake 2: Bypassing the factory. Calling new JerseyDesign(...) directly in client code creates private copies and quietly destroys the sharing. All creation must go through the factory's cache โ€” that is the entire enforcement mechanism.

Mistake 3: Putting extrinsic data into the flyweight. If the flyweight stores fanName, then two fans asking for the same design suddenly need two flyweights, and sharing collapses. Anything that differs per object must stay outside.

Mistake 4: Applying it everywhere. Flyweight makes code harder to read โ€” new readers wonder why a Jersey does not know its own design fields. Pay that cost only when measurements show memory is actually the problem.

Mistake 5: Forgetting cache growth. The factory cache holds flyweights forever by default. If intrinsic keys are open-ended (one design per user!), the "memory saver" becomes a memory leak. Keep the distinct-design count small and bounded, or add an eviction rule.

Compare with cousins

Students mix Flyweight up with Singleton, Prototype, and object pools. The table clears it up:

QuestionFlyweightSingletonPrototypeObject Pool
How many instances?Few โ€” one per distinct intrinsic stateExactly oneAs many clones as you makeA fixed set, borrowed and returned
Mutable?Never (must be immutable)Often mutableClones are independent and mutableReset between uses
GoalSave memory by sharingSingle global access pointCheap copies of a templateAvoid creation/destroy cost
Sharing directionMany contexts โ†’ one objectAll code โ†’ one objectNone โ€” copies are separateTemporary exclusive use

Other useful relations:

  • The flyweight factory is itself often a Singleton โ€” one cache for the whole app.
  • Composite trees use flyweights for leaf nodes: a document's thousands of identical characters point at shared glyph objects while the tree keeps the structure.
  • Facade is the opposite end of the spectrum: Facade makes one big helpful object in front of a subsystem; Flyweight makes many tiny shared objects behind your data.

Everything on one tree before the revision box:

Figure 9: The whole Flyweight pattern as a mind map

Quick revision box

+--------------------------------------------------------------+
|                  FLYWEIGHT โ€” QUICK REVISION                   |
+--------------------------------------------------------------+
| Idea      : Share the heavy common data; keep only the tiny  |
|             unique part per object. Saves huge RAM.          |
| Nickname  : Cache                                            |
| Analogy   : One cricket jersey DESIGN shared by all players; |
|             only name + number differ per jersey.            |
| Intrinsic : inside / identical / immutable -> in flyweight   |
| Extrinsic : external / exclusive -> in context or params     |
| Parts     : Flyweight, FlyweightFactory(cache), Context      |
| Iron rule : Flyweight is IMMUTABLE. Factory only creates.    |
| The proof : contexts = 10,000 ... flyweights = 3             |
| Memory    : N x shared  ->  N x pointer + D x shared         |
| Trade-off : Saves RAM, may spend CPU passing extrinsic data  |
| Real life : String interning, Java Integer cache (-128..127),|
|             game sprites/meshes drawn at many positions      |
+--------------------------------------------------------------+

Practice exercise ๐Ÿ“

  1. Textbook printing press, extended. Take the Python press above and add a fifth design mid-run. Print the proof lines again and confirm the plate count went from 4 to 5 while the copy count grew freely. Then compute, with the College corner formula, the memory saved if each plate is 40 MB and each copy record is 60 bytes.
  2. Chess pieces. A chess server hosts 10,000 games. Each game has up to 32 pieces, but there are only 12 kinds of piece (6 types ร— 2 colours). Build a PieceType flyweight (type, colour, movementRules) and a Piece context (square, gameId). Show that 10,000 games share just 12 flyweights.
  3. Challenge โ€” catch the bug. Take your exercise 1 code and deliberately add a setter set_title() to the flyweight. Change the title through one copy and print two copies from different schools. Watch both change. Write two sentences explaining why immutability is the flyweight's most important rule.
  4. College challenge โ€” find the break-even. Using the formula naive = N ร— (u + s) and flyweight = N ร— (u + 8) + D ร— s, derive the value of s (shared bytes) below which the pattern saves less than 10% for your N, u, and D. This one calculation will save you from ever applying Flyweight where it cannot pay.

Frequently asked questions

What is the Flyweight pattern in simple words?
Flyweight saves memory by sharing the common, never-changing data between many objects instead of copying it into each one. Like one cricket jersey design shared by the whole team, while only the name and number differ per player.
What are intrinsic and extrinsic state?
Intrinsic state is the shared, constant data stored once inside the flyweight (the jersey design). Extrinsic state is the unique, per-object data kept outside and passed in when needed (each player's name and number).
Why must a flyweight be immutable?
Because one flyweight object is shared by thousands of contexts. If anyone could change it, all of them would change silently at once. Sharing is only safe when the shared data can never be modified.
When should I NOT use Flyweight?
When you have few objects, or when each object's data is genuinely unique. With nothing big and repeated to share, the pattern only adds complexity without saving memory.
Where does Flyweight appear in real software?
String interning in Java and Python, the Java Integer cache for values -128 to 127, and game engines that load one sprite or mesh once and draw it at thousands of positions.

Further reading

Related Lessons