{} The Go Reference

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 ⏱ 3 min read 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:

flyweight.go — editable & runnable
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.)

In the standard library

Go does flyweight-style sharing in a few places — and now offers it as a built-in:

  • unique (Go 1.23+)unique.Make(v) interns a comparable value, returning a Handle that de-duplicates identical values across the whole program. That’s Flyweight as a standard, concurrency-safe API — perfect for canonicalizing many repeated strings, keys, or small structs.
  • Strings are inherently flyweight-flavored: immutable, and a substring shares the original’s backing array instead of copying.
  • net/netip.Addr was designed as a small, comparable, shareable value precisely to avoid the per-object overhead of the older net.IP ([]byte).

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 / 5

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.

4. How is Flyweight different from sync.Pool?

Flyweight = many objects pointing at the *same* shared, read-only value, concurrently. sync.Pool = a free-list of reusable scratch objects you Get/Put, where each object has a single owner at a time. Sharing immutable state vs recycling mutable buffers — opposite use cases.

5. Your flyweight factory's cache is hit from many goroutines. What do you need?

Go maps are not safe for concurrent write, so a shared factory cache needs a mutex or sync.Map. The standard unique package (Go 1.23) does exactly this interning safely for you.

Comments

Sign in with GitHub to join the discussion.