If youâve ever built something that renders thousands of icons and noticed most of them are the same image just sitting at a different position, this is for you. The file explorer example is exactly that: a thousand files, but only four file types and three folder colors, so thereâs no reason to load the same icon image a thousand times.
The problem
You need a large number of similar objects, and most of their state is actually identical across instances. Storing that identical state once per object is wasted memory that scales with object count instead of with the actual variety of data.
Without the pattern
The obvious thing is to skip the factory and the cache entirely, and let each FileSystemItem build its own icon in its constructor: new FileIcon(fileType) or new FolderIcon(color), right there, one per item. That compiles fine, and every icon draws correctly. The problem is what it does to the object graph once youâre past a handful of files. A FileIcon stores type and builds image as "Image for " + type + " files" in its own constructor, so a thousand .txt files means a thousand separate FileIcon objects, each carrying its own independently-allocated copy of the exact same type string and the exact same image string. Same story for folders keyed on color. Youâre not storing four file-type icons and three folder-color icons anymore, youâre storing one heavyweight icon per file on disk, and the âsame type, same image, same everythingâ state gets duplicated on every single instance instead of built once.
Nothing about this is functionally wrong, it just means memory scales with file count instead of with the actual variety of icons in play, and those two numbers can be wildly different. Ten thousand files backed by four distinct images should cost you four images, not ten thousand.
flowchart TD
subgraph dup["One FileIcon object per file, same data every time"]
F1["FileSystemItem: report.txt<br/>new FileIcon('TXT')<br/>type='TXT', image='Image for TXT files'"]
F2["FileSystemItem: notes.txt<br/>new FileIcon('TXT')<br/>type='TXT', image='Image for TXT files'"]
F3["FileSystemItem: todo.txt<br/>new FileIcon('TXT')<br/>type='TXT', image='Image for TXT files'"]
end
F1 -. same data, different object .-> F2
F2 -. same data, different object .-> F3
With the pattern
Icon is the flyweight interface: draw(int x, int y). Notice x and y are parameters, not fields, thatâs the extrinsic state, unique per usage, passed in at call time rather than stored on the flyweight.
FileIcon and FolderIcon are the concrete flyweights. FileIcon stores type and image as its intrinsic state, the constructor builds image once as "Image for " + type + " files". FolderIcon stores color and image the same way. Both are the same for every instance representing the same type or color, which is exactly why theyâre safe to share.
IconFactory is the flyweight factory, holding a static Map<String, Icon> iconCache. getFileIcon(String fileType) builds a key like "FILE_" + fileType.toUpperCase(), checks the cache, and only calls new FileIcon(fileType) if that key isnât already there. getFolderIcon(String color) does the same with a "FOLDER_" prefix. Every subsequent call for the same type or color returns the identical cached instance rather than building a new one, you can see that directly in the test file, IconFactory.getFileIcon("TXT") called three times in a row returns the same object, same hash code, every time.
FileSystemItem is the context: it holds name, x, y (the extrinsic state, unique per item) and a reference to a shared Icon. Its display() method prints the name and calls icon.draw(x, y), handing the extrinsic state to the flyweight at the moment itâs needed. A thousand FileSystemItem instances can all point at the same handful of Icon objects.
classDiagram
class Icon {
<<interface>>
+draw(x: int, y: int)
}
class FileIcon {
-type: String
-image: String
+draw(x: int, y: int)
+getType() String
}
class FolderIcon {
-color: String
-image: String
+draw(x: int, y: int)
+getColor() String
}
class IconFactory {
-iconCache: Map~String, Icon~$
+getFileIcon(fileType: String)$ Icon
+getFolderIcon(color: String)$ Icon
+displayCacheStats()$
}
class FileSystemItem {
-name: String
-x: int
-y: int
-icon: Icon
+display()
+getName() String
}
Icon <|.. FileIcon
Icon <|.. FolderIcon
IconFactory ..> Icon
FileSystemItem o-- Icon
What it costs you
Splitting Icon into intrinsic state that lives on the shared object (type, image) and extrinsic state that gets passed into draw() on every call (x, y) isnât free, itâs a design decision you have to get right up front, and the split doesnât always fall out cleanly from how the domain wants to be modeled. FileIcon and FolderIcon only work as shared, cached objects because nothing about them ever changes after the constructor runs, if draw() ever needed to mutate image based on something item-specific, every FileSystemItem pointing at that same cached FileIcon would start stepping on every other one, one callerâs change leaking into another callerâs icon with no exception thrown to tell you it happened. Thatâs the trade: the shared flyweight has to be treated as immutable for as long as it lives in iconCache, and anything thatâs even slightly per-instance has to be pulled out of the object and threaded through as a parameter instead, which is why draw(x, y) carries arguments a non-shared Icon would never need, and why every caller of IconFactory has to remember that what getFileIcon("TXT") hands back isnât theirs alone.
When to reach for it
- Youâve got a large object count where most of the per-object state is actually identical across many instances.
- The identical portion is safely immutable, sharing it canât cause one callerâs mutation to leak into anotherâs.
- The unique-per-instance part (position, name, whatever) is small enough to hand in as a parameter instead of storing on the shared object.
The takeaway
The whole pattern hinges on correctly splitting state into intrinsic and extrinsic. Get that split wrong, put something that should be per-instance into the shared flyweight, and youâve built a bug where one callerâs data bleeds into anotherâs, not a memory optimization.
Read the full source on GitHub.
Comments