skip to content

When would you design your own Flyweight cache like the Integer cache, and what are the tradeoffs and risks?

level: principalimportance: nice to knowfreq 30%

answer

  1. only when many identical immutable objects + measured allocation cost
  2. factory like valueOf, hide constructor
  3. bounded: fixed set / LRU / weak interning
  4. thread-safe shared cache
  5. document equals-not-== ; prefer enums for closed sets

basics

~20 s

Build your own cache when you create many identical, immutable objects and allocation/memory is a real cost — for example interning common values. Make the objects immutable, document that == is unreliable, and avoid leaking mutable or unbounded caches.

solid answer

~50 s

Roll a Flyweight cache when profiling shows you allocate huge numbers of equal, immutable instances — value objects, enum-like codes, parsed tokens, color/point objects. Provide a factory (like valueOf) that returns shared instances for the common set and fresh ones otherwise. The objects MUST be immutable, or sharing corrupts callers. Key tradeoffs: you save allocations and GC pressure and may improve cache locality, but you add lookup cost, a long-lived cache that can pin memory (a leak if unbounded), and thread-safety concerns on the cache map. The biggest risk is the same == trap the Integer cache creates: callers may start depending on identity, which holds only for cached instances. So keep the cache bounded, prefer eager fixed sets or weak interning (like String.intern semantics), document equals-not-==, and measure before adding it — premature interning often costs more than it saves.

go deeper

for a junior

Understands that you'd cache to avoid creating the same object many times, and that the objects must be immutable.

for a middle

Can sketch a valueOf-style factory and name the basic tradeoff of saved allocations vs lookup cost.

for a senior

Weighs GC/memory wins against retention, thread-safety, and the == identity trap; chooses eager fixed vs weak interning and documents equals-not-==.

for a principal

Drives the build/don't-build decision from profiling, sets governance (identity is not a contract, prefer enums, bound the cache), and reasons about contention, escape analysis, and long-term maintenance cost across the org.

## The decision: should you build a Flyweight? Flyweight is justified only when **all** of these hold: 1. You create a **large number** of objects. 2. Many of them are **value-identical** (the same logical value recurs constantly). 3. The objects are (or can be made) **immutable**. 4. Allocation/memory/GC is a **measured** bottleneck — not a guess. The JDK's `Integer` cache is the textbook case: programs box the same small integers millions of times. Your own candidates look similar: interned domain codes (currency, country), parsed token objects, frequent small `enum`-like values, repeated coordinate/color objects in graphics. ## How to build one Provide a **static factory** rather than a public constructor, so callers can't bypass the cache: ```java public final class Color { // immutable private final int rgb; private Color(int rgb) { this.rgb = rgb; } private static final Map<Integer, Color> CACHE = new ConcurrentHashMap<>(); public static Color of(int rgb) { // like Integer.valueOf return CACHE.computeIfAbsent(rgb, Color::new); } } ``` Design choices mirror the JDK's: - **Eager fixed set** (pre-build the common values, as `IntegerCache` does) — predictable, bounded, fast. - **Lazy interning** (`computeIfAbsent`) — covers an open value space but the cache can grow unbounded. ## Tradeoffs **Wins:** - Fewer allocations -> less GC churn, lower latency jitter. - Lower steady-state memory when duplicates dominate. - Possibly better cache locality / dedup downstream (e.g. dedup'd map keys). **Costs and risks:** - **Lookup overhead** — every creation now pays a map/array probe; for cheap objects this can *cost more* than the allocation it avoids. - **Memory pinning / leak** — a long-lived, unbounded cache keeps objects alive forever; an open key space turns the 'cache' into a leak. Bound it (fixed set, size cap/LRU, or `WeakReference`/`WeakHashMap` interning like `String.intern`). - **Thread safety** — the cache is shared mutable state; use a concurrent structure or immutable precomputed arrays. A non-threadsafe map under contention corrupts or races. - **The `==` identity trap** — this is the deepest risk and the direct lesson of `Integer`. Once some instances are shared, callers may *notice* that `==` works for them and start relying on identity — which then breaks for non-cached values, exactly the -128/128 bug, but now in your domain. You inherit a perpetual support burden. ## Governance and documentation - **Document loudly** that the type's identity is **not** a contract: 'compare with `equals()`; `==` is unspecified.' Override `equals`/`hashCode` correctly so value comparison is the only sanctioned path. - Prefer **enums** when the value set is small and closed — the JVM already gives you canonical, identity-safe singletons with no cache to maintain. - **Measure first.** Like all interning (including overuse of `String.intern`), premature Flyweight commonly adds complexity, contention, and retention bugs for no real gain. Add it behind a benchmark, keep it bounded, and revisit with profiling. ## When NOT to do it Mutable objects, low duplication, cheap-to-allocate objects, or unbounded/short-lived value spaces — in all these the cache is pure downside (lookup cost, retention, complexity). Default to plain allocation and let escape analysis / the GC do their job until data says otherwise.

  • Why are enums often a better answer than a hand-rolled Flyweight cache?
    For a small, closed set of values, enums give you canonical, identity-safe singletons created by the JVM, with no cache, no thread-safety code, and no retention/leak risk — and == is actually safe by design.
  • How do you prevent a lazy interning cache from leaking memory?
    Bound it: cap size with an LRU, use weak/soft references (WeakHashMap-style interning like String.intern), or restrict to a precomputed fixed set so it can never grow unbounded.

saying these in an interview costs you the question

  • Caching mutable objects
  • Unbounded lazy cache over an open key space (memory leak)
  • Adding interning without profiling (premature optimization)
  • Letting callers rely on == for cached instances
  • Ignoring thread-safety of the shared cache map

context