A document holds a million characters, but only a few dozen distinct ones, and a naive object-per-character design stores the same font data a million times. Flyweight is the Gang of Four structural pattern that supports huge numbers of fine-grained objects by sharing the parts that repeat, and the classic example is exactly this one, the text editor whose glyphs would otherwise drown the heap.
A flyweight is really just an object split in two. The half that repeats across occurrences, called intrinsic state, gets stored once and shared. The half that differs, the extrinsic state, gets carried by whoever uses the object and passed in at call time. Memory then scales with distinct values instead of total occurrences, which is the entire trick.
Here's the unshared version at toy scale. The text hello, hello has 12 characters drawn from 6 distinct ones, and the document builds a fresh Glyph per character, each hauling its own copy of the font configuration:
Twelve placements, twelve glyph objects, and the identity check between the two l's comes back false because each is a private copy. At twelve characters nobody cares. At a million characters the duplicated font data is nearly all of the allocation, purely redundant, and the garbage collector gets to chew through a million objects that contain perhaps 60 distinct ones wearing different positions.
The flyweight version splits the glyph. Character and font stay inside Glyph as the shared, immutable, intrinsic half, position moves out into the document as the extrinsic half, and a GlyphFactory with a pool becomes the only source of glyphs. Before reading past the code, predict two numbers and a boolean: placements, pool size, and what the identity check on the two l's says now:
Twelve placements, a pool of 6, and True: both l's are literally the same object, requested twice from the factory. Scale the arithmetic and the pattern shows its teeth: a million-character document still carries a million tiny placements, but the heavy glyph objects number only as many as the distinct characters used, and adding ten thousand more l's allocates nothing new at all.
Three roles. The Flyweight (Glyph) holds intrinsic state only, immutable by contract. The FlyweightFactory (GlyphFactory) owns the pool, keyed by intrinsic value, and guarantees that equal requests return the same object rather than equal ones. The Client holds the extrinsic state and pairs it with shared flyweights at use time, the way the document pairs glyphs with positions.
The pattern works like a set of rubber stamps. One stamp per letter lives in the drawer, and a page of text is a sequence of pressings, each pairing some stamp with some position, with no one carving a new stamp per impression. The analogy has a limit that points at the pattern's sharpest rule: a real stamp can be re-inked or filed down, but a flyweight must never change, because a modified stamp would silently alter every impression that was ever going to be made with it. Shared and mutable is the one combination the pattern cannot survive.
Installing the pattern is four moves:
Glyph) keeps only intrinsic state and never mutates, because an object shared by 12 placements that changes once has changed in twelve places at once. Immutability is what makes the sharing safe.GlyphFactory.get(char) checks its map and returns the existing glyph or creates the first one. The factory is the sharing guarantee, which only holds while it stays the single place glyphs come from.Java runs a flyweight pool you've used whether or not you knew it. Integer.valueOf documents that it always caches the values from -128 to 127, so every autoboxed 100 in a program is the same object, while 1000 falls outside the pool and allocates fresh each time. The memory effect at scale is the glyph story again, and as a ballpark model, a million boxed small integers as separate objects would occupy somewhere near 16 megabytes, while the shared pool caps the distinct objects at 256, a few kilobytes, with the million references pointing into it.
The same cache is also the classic footgun. Compare boxed integers with == and the program works perfectly for every value tested in development, because small test values live in the pool, then fails in production the day a value crosses 127 and identity stops meaning equality. Sharing changed what == measures, and that change is part of the pattern's price everywhere it's used.
Go builds the factory from a map and, under concurrency, a mutex or sync.Map guarding it. The pool is map[rune]*Glyph, the sharing is pointer reuse, and the identity check is plain pointer equality, all visible in the code tab with nothing imported beyond the standard library.
Rust names the sharing machinery explicitly. Rc documents itself as shared ownership where cloning produces a new pointer to the same allocation, which is the flyweight hand-out in one method call: the factory stores Rc<Glyph> and serves out Rc::clone handles that cost a reference-count bump, not a copy. The immutability rule comes free, since shared references forbid mutation unless someone reaches for interior mutability on purpose, and Arc swaps in when threads enter the picture.
You might think this is caching with a fancier name, and the factory's pool certainly looks like one. The difference is the split. A cache stores whole answers to avoid recomputing them, and what it stores can be anything, mutable included. A flyweight requires the intrinsic-extrinsic separation: the shared object must be context-free and frozen, and the varying context must arrive from outside at every use. Skip the split and share whole mutable objects, and what you have is a cache with aliasing bugs waiting inside it.
The other neighbor is Singleton, since both bound how many instances exist. The units differ: a singleton is one instance per type, full stop, while a flyweight pool holds one instance per distinct intrinsic value, happily thousands of them, each shared by every occurrence of that value. A singleton glyph would mean one glyph total, which is a very minimalist font.
The mechanics are a map and a rule, and the rule is where the bugs live:
l everywhere, while its position belongs to one spot in one document. State that depends on where the object is used can't ride along in a shared object, no matter how convenient the field would be.Two questions before you go. First: the document holds 12 placements but only 6 glyphs. Where does each l's position live, and why would moving it onto the glyph break the whole arrangement? Second: Flyweight and Singleton both restrict how many instances exist, so what is each one's unit of uniqueness? Both answers are on this page.
Then go spot it in the wild. You've seen Java's integer cache above, and the same move powers game engines sharing one mesh across a thousand rendered trees and map applications sharing tile imagery across views. In your own codebase, find the largest collection of objects and ask which fields are identical across every element. Those fields are intrinsic state paying rent in every object, and the flyweight split is how they stop.
class Glyph:
def __init__(self, char: str):
self.char = char
# The heavy part, duplicated into every single object
self.font = "Inter, 16px, hinted"
text = "hello, hello"
document = [(Glyph(char), x) for x, char in enumerate(text)]
print(len(document)) # 12 placements, 12 glyph objects
print(document[2][0] is document[3][0]) # False: every l is its own copy
# Scale it up: a million characters means a million copies of
# the same font data that six shared objects could have carriedclass Glyph: # Flyweight: intrinsic state only, shared and immutable
def __init__(self, char: str):
self.char = char
self.font = "Inter, 16px, hinted" # now stored once per distinct char
class GlyphFactory: # FlyweightFactory: the only door to glyphs
def __init__(self):
self._pool: dict[str, Glyph] = {}
def get(self, char: str) -> Glyph:
# One glyph per distinct character, ever
if char not in self._pool:
self._pool[char] = Glyph(char)
return self._pool[char]
def pool_size(self) -> int:
return len(self._pool)
# Example usage: the document pairs shared glyphs with extrinsic positions
factory = GlyphFactory()
text = "hello, hello"
document = [(factory.get(char), x) for x, char in enumerate(text)]
print(len(document)) # 12 placements
print(factory.pool_size()) # 6 glyphs: h e l o comma space
print(document[2][0] is document[3][0]) # True: both l's share one glyph