skip to content

Your domain requires keys whose identifying fields legitimately change over time, yet you need them in a HashMap. How do you design around the mutable-key hazard?

level: seniorimportance: should knowfreq 35%

answer

  1. Decouple identity from value
  2. Strategy 1: immutable id drives equals/hashCode (default)
  3. Strategy 2: remove → mutate → re-insert (atomic!)
  4. Strategy 3: immutable snapshot as key
  5. Strategy 4: id-index / sorted with stable comparator

basics

~10 s

Don't let the changing fields drive hashCode. Either key the map on a separate stable id (storing the object as the value), use the remove-mutate-reinsert pattern, or make a snapshot/immutable copy as the key.

solid answer

~50 s

Several strategies, picked by how the mutation is shaped. First and best: separate identity from value — key the map on an immutable id (a UUID or DB key assigned once) and base hashCode/equals on that id only, so the mutable business fields can change freely without affecting bucketing. Second: if you must key on the changing field itself, use the explicit remove → mutate → re-insert sequence so the collection re-buckets the entry; this is error-prone and must be done under proper locking if concurrent. Third: store an immutable snapshot (a copy or record) as the key so the live object can mutate while the key stays fixed. Fourth: reconsider the data structure — a sorted structure with a stable comparator, or an external index keyed by id, may fit better. The anti-pattern is leaving a mutable field in hashCode and hoping no one mutates a stored key. I'd default to id-based identity; it removes the whole hazard class.

go deeper

for a junior

Knows the safe move is to avoid mutating keys and to prefer immutable keys; may not yet design the decoupling.

for a middle

Can apply remove-mutate-reinsert and explain why a separate stable id avoids the problem.

for a senior

Chooses among id-based identity, snapshot keys, and external indexes per the situation, handles the concurrency race, and knows TreeMap shares the hazard.

for a principal

Sets an architecture-wide identity strategy (stable ids, immutable value types), evaluates the trade-off of entity vs value identity across persistence/caching/distribution, and ensures invariants are encapsulated so callers can't reintroduce the bug.

## The core conflict Hash collections require a stored key's hashCode to be **constant**, but the domain says the identifying data **changes**. You can't have both at once on the same fields, so the design must decouple 'what identifies the entry in the map' from 'what changes'. ## Strategy 1 — id-based identity (the default, strongly preferred) Give the object a **stable, immutable id** assigned at construction (UUID, sequence, DB primary key). Base `equals`/`hashCode` on the id alone. Now the mutable business fields can change all they like; the id never does, so the hashCode is constant and the entry stays findable. This converts an 'identity changes' problem into 'a value field changed', which hash collections don't care about. Cost: you've chosen *entity identity* (same id = same entity) over *value equality*; make sure that's the semantics you want. ## Strategy 2 — remove → mutate → re-insert If the key genuinely *is* the changing field (e.g. you key a map by a name that can be renamed), wrap each mutation: ``` map.remove(oldKey); key.setName(newName); map.put(key, value); ``` This is the only way to make the collection re-bucket. Pitfalls: it's easy to forget the remove step; it must be **atomic** under concurrency (hold a lock around all three operations, or the entry can be seen in an inconsistent state or lost by a racing thread); and any code path that mutates the key *without* going through this wrapper reintroduces the bug. Encapsulate it behind a method so callers can't bypass it. ## Strategy 3 — immutable snapshot as key Keep the live, mutable object elsewhere and use an **immutable copy** (a `record`, or a defensively-copied value object) capturing the identifying fields *at insertion time* as the key. The live object mutates; the key, being a snapshot, never does. You re-key (remove old snapshot, insert new) only when you intend to reflect a change. This makes the 'when does the map see the change' decision explicit. ## Strategy 4 — pick a different structure / external index Sometimes the right answer is not a `HashMap` keyed on the mutable object. Options: maintain a `Map<Id, Entity>` (id→object) plus secondary indexes you rebuild on change; or use a sorted structure with a **stable comparator** (note `TreeMap` has the analogous hazard if the comparator reads mutable fields, so the comparator must use stable fields too). For multi-field, change-tolerant lookup, a small in-memory index layer or a database is often cleaner than coercing a hash map. ## Concurrency note Under concurrency the remove/re-insert window is a race: between `remove` and `put`, another thread can miss the key or double-insert. Use a `ConcurrentHashMap` with `compute`/atomic operations where possible, or guard the re-key in a single critical section. id-based identity (Strategy 1) sidesteps this entirely because nothing is ever re-keyed. ## Decision guide - Have a natural stable id? → **Strategy 1**, done. - Must key on the mutable field, single-threaded? → **Strategy 2**, encapsulated. - Want explicit control of when changes propagate? → **Strategy 3**. - Complex/multi-field/concurrent lookup? → **Strategy 4** (external index / proper store). The one thing you never do: include the mutable field in `hashCode` and leave the entry to be mutated in place.

  • What makes the remove-mutate-reinsert pattern dangerous under concurrency?
    The three steps aren't atomic: between remove and put another thread can fail to find the key, see a stale value, or insert a duplicate. You must perform the re-key inside a single critical section or via atomic map operations.
  • Does TreeMap escape the mutable-key hazard?
    No — it has the analogous problem: if the Comparator/compareTo reads a field that mutates, the tree ordering becomes inconsistent and lookups/removals can fail. The comparator must use stable fields.

saying these in an interview costs you the question

  • Leaving a mutable field in hashCode and assuming 'we just won't mutate stored keys'.
  • Doing remove/re-insert without locking in a concurrent context.
  • Assuming TreeMap/sorted structures are immune — they aren't if the ordering uses mutable state.
  • Mutating the key object but forgetting the remove step entirely.

context