When would you design your own Flyweight cache like the Integer cache, and what are the tradeoffs and risks?
answer
- only when many identical immutable objects + measured allocation cost
- factory like valueOf, hide constructor
- bounded: fixed set / LRU / weak interning
- thread-safe shared cache
- document equals-not-== ; prefer enums for closed sets
basics
~20 sBuild 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 sRoll 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
Understands that you'd cache to avoid creating the same object many times, and that the objects must be immutable.
Can sketch a valueOf-style factory and name the basic tradeoff of saved allocations vs lookup cost.
Weighs GC/memory wins against retention, thread-safety, and the == identity trap; chooses eager fixed vs weak interning and documents equals-not-==.
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