Structural pattern · Gang of Four · Advanced

Flyweight

Share common immutable state across many objects to slash memory use, keeping only per-instance data separate.

Structural Advanced Complete

🅰️ Analogy

A text editor showing a million letters doesn’t store a full font definition for each one. Every ‘a’ shares a single glyph object — shape, font, metrics — and only the position of each letter is stored per occurrence. One heavy thing shared, one light thing repeated.

The problem

You have a huge number of objects, and most of each object is identical, immutable data. Storing that data per object burns memory. Flyweight splits each object’s state into intrinsic (shared, immutable — stored once) and extrinsic (per-instance — kept outside), and hands out shared instances through a factory.

Structure

classDiagram
  class TreeFactory {
    -cache map
    +GetTreeType(name, color) TreeType
  }
  class TreeType {
    +Name string
    +Color string
    +Texture string
  }
  class Tree {
    -x int
    -y int
    -kind TreeType
  }
  TreeFactory ..> TreeType : caches & shares
  Tree o--> TreeType : references (intrinsic)
  note for TreeType "shared, immutable"

Idiomatic Go

A factory caches and returns shared TreeType values (interning). A thousand trees, but only two heavy TreeType objects exist. Edit and Run:

package main

import "fmt"

// Flyweight: heavy, shared, IMMUTABLE state.
type TreeType struct {
	Name    string
	Color   string
	Texture string // imagine this is large
}

// Factory caches TreeTypes so identical ones are shared, not duplicated.
var treeTypes = map[string]*TreeType{}

func GetTreeType(name, color, texture string) *TreeType {
	key := name + "|" + color
	if t, ok := treeTypes[key]; ok {
		return t // reuse the shared instance
	}
	t := &TreeType{name, color, texture}
	treeTypes[key] = t
	return t
}

// Tree holds only extrinsic state (position) plus a pointer to the flyweight.
type Tree struct {
	x, y int
	kind *TreeType
}

func main() {
	var forest []Tree
	for i := 0; i < 1000; i++ {
		kind := GetTreeType("Oak", "green", "…big texture blob…")
		if i%2 == 0 {
			kind = GetTreeType("Pine", "dark-green", "…big texture blob…")
		}
		forest = append(forest, Tree{x: i, y: i * 2, kind: kind})
	}

	fmt.Printf("trees in forest:                 %d\n", len(forest))
	fmt.Printf("distinct TreeType objects alloc: %d\n", len(treeTypes))
}

🐹 It's interning + a state split

Flyweight in Go is a cache of shared immutable values plus the discipline of keeping per-instance data (here, x, y) out of the shared object. Go’s strings are already flyweight-flavored — immutable, and substrings share backing memory. If the factory is used concurrently, guard the cache with a sync.Mutex or use sync.Map. (Don’t confuse this with sync.Pool, which reuses objects to cut allocations rather than sharing immutable state.)

Pitfalls

⚠️ Profile first; immutability is non-negotiable

Flyweight adds a cache and a layer of indirection — only worth it when duplicated state genuinely dominates memory, which you should confirm with pprof, not a hunch. And the shared object must be treated as read-only: a single mutation leaks into every object that points at it. If you need per-object changes, that data is extrinsic and belongs outside the flyweight.

When to use it — and when not

✅ Reach for it when

  • You hold a very large number of objects that share a lot of identical, immutable data.
  • Memory is the bottleneck and profiling shows that duplicated state dominates.
  • The shared part is large relative to the per-instance part.

⛔ Think twice when

  • There are only a few objects, or the shared data is small — the cache costs more than it saves.
  • The 'shared' state is mutable — flyweights must be immutable, or you get spooky cross-talk.
  • You're optimizing before profiling.

Check your understanding

Score: 0 / 3

1. What's the difference between intrinsic and extrinsic state?

Flyweight splits state: the heavy, identical part (a glyph, a tree type) is stored once and shared; the part that differs per object (a position) is kept outside, often passed as an argument.

2. What does Flyweight optimize?

It trades a little indirection for big memory savings when millions of objects would otherwise each carry a copy of the same large immutable data.

3. Why must flyweights be immutable?

A flyweight is referenced by many objects at once. If it were mutable, changing it for one would change it for all — so shared intrinsic state must be read-only.