I. Flyweight — Structural Pattern
The Flyweight pattern is a structural design pattern that reduces memory consumption by sharing as much state as possible between similar objects. Instead of storing all data in each object, Flyweight splits state into two categories:
- Intrinsic state — data that is shared across many objects; stored once inside the flyweight object (e.g. tree species, color, texture)
- Extrinsic state — data that is unique to each object instance; passed in by the caller at runtime (e.g. x, y position on the map)
The flyweight object itself is immutable and reusable. A factory (or cache) ensures that only one flyweight instance exists per unique combination of intrinsic state.
Use Flyweight when:
- You need to create a very large number of similar objects and memory is a concern
- Most of the object's state can be made extrinsic (moved outside the object)
- Many groups of objects share the same intrinsic data
II. Real-world Example
A forest rendering system with thousands of trees:
- A forest can have thousands of
Treeobjects, but trees of the same species share identical data: name, color, texture
- Without Flyweight: 1,000 trees × 3 fields = 3,000 strings in memory
- With Flyweight: only 2–3
TreeTypeobjects exist, regardless of how many trees there are
- Each
Treestores only its position (x, y) and a pointer to the sharedTreeType
GetTreeType()acts as the factory: returns an existingTreeTypefrom cache, or creates a new one

III. Implementation
tree_type.go — the flyweight (intrinsic state)
package flyweight
import "fmt"
// TreeType holds intrinsic (shared) state — species, color, texture.
// Many Tree instances can share a single TreeType.
type TreeType struct {
Name string
Color string
Texture string
}
func (t *TreeType) Render(x, y int) {
fmt.Printf("Rendering tree [%s | color:%s | texture:%s] at (%d, %d)\n",
t.Name, t.Color, t.Texture, x, y)
}tree.go — the context (extrinsic state)
package flyweight
// Tree holds extrinsic (context-specific) state — position on the map.
// It delegates rendering to the shared TreeType flyweight.
type Tree struct {
X int
Y int
TreeType *TreeType
}
func (t *Tree) Render() {
t.TreeType.Render(t.X, t.Y)
}tree_factory.go — the flyweight factory/cache
package flyweight
import "fmt"
var treeTypes = map[string]*TreeType{}
// GetTreeType returns a cached TreeType or creates a new one if not found.
// This is the core of the Flyweight pattern — reuse shared instances.
func GetTreeType(name, color, texture string) *TreeType {
key := fmt.Sprintf("%s-%s-%s", name, color, texture)
if tt, ok := treeTypes[key]; ok {
return tt
}
tt := &TreeType{Name: name, Color: color, Texture: texture}
treeTypes[key] = tt
fmt.Printf("Created new TreeType: %s\n", key)
return tt
}
func TreeTypeCount() int {
return len(treeTypes)
}forest.go — the client
package flyweight
// Forest manages a large collection of Tree objects.
// Despite having thousands of trees, only a handful of TreeType objects exist.
type Forest struct {
trees []*Tree
}
func (f *Forest) PlantTree(x, y int, name, color, texture string) {
tt := GetTreeType(name, color, texture)
f.trees = append(f.trees, &Tree{X: x, Y: y, TreeType: tt})
}
func (f *Forest) Render() {
for _, t := range f.trees {
t.Render()
}
}
func (f *Forest) Count() int {
return len(f.trees)
}IV. Explain the example above
Let's trace through planting 1,000 trees of 2 types:
f := &Forest{}
for i := 0; i < 500; i++ {
f.PlantTree(i, i*2, "Oak", "dark-green", "rough")
}
for i := 0; i < 500; i++ {
f.PlantTree(i, i*3, "Pine", "light-green", "smooth")
}
fmt.Println("Trees:", f.Count()) // 1000
fmt.Println("TreeTypes:", TreeTypeCount()) // 2
f.Render()Output (first few lines):
Created new TreeType: Oak-dark-green-rough
Created new TreeType: Pine-light-green-smooth
Rendering tree [Oak | color:dark-green | texture:rough] at (0, 0)
Rendering tree [Oak | color:dark-green | texture:rough] at (1, 2)
Rendering tree [Oak | color:dark-green | texture:rough] at (2, 4)
...
Rendering tree [Pine | color:light-green | texture:smooth] at (0, 0)
...Step by step:
- First call
PlantTree(0, 0, "Oak", "dark-green", "rough")— key"Oak-dark-green-rough"not in cache → creates newTreeType, stores it, logs"Created new TreeType"
- Second call
PlantTree(1, 2, "Oak", "dark-green", "rough")— same key, cache hit → returns existingTreeTypepointer. No new object created.
- This repeats for all 500 Oak trees — only 1
TreeTypeobject ever exists for Oak
- First Pine call creates a second
TreeType; all 500 Pine trees share it
TreeTypeCount() == 2whilef.Count() == 1000
f.Render()iterates all 1,000Treeobjects, each callingt.TreeType.Render(t.X, t.Y)— the position (extrinsic) is passed at call time, not stored inTreeType
The memory saving: instead of 1,000 × 3 strings, you have 2 × 3 strings + 1,000 × (2 ints + 1 pointer). As tree count grows, the TreeType pool stays constant.
V. Conclusion
The Flyweight pattern is a memory optimization first and foremost. It trades a small amount of design complexity (separating intrinsic/extrinsic state) for significant memory savings at scale.
Trade-offs:
- Good fit: rendering systems (game engines, map tiles, text glyphs, particle systems); any scenario with large numbers of nearly-identical objects where memory is constrained
- Poor fit: when objects don't actually share much state, or when the overhead of the factory cache and separating state adds complexity without meaningful savings; also adds thread-safety concerns if the cache is global and mutated concurrently
One thing to watch: the global treeTypes map in this implementation is not thread-safe. In concurrent scenarios, you'd want a mutex around reads and writes to the cache.
No pattern is universally superior. Use Flyweight when you can measure a real memory problem, not just as a premature optimization.
VI. References
- Go Design Patterns — Mario Castro Contreras



