Free lesson · Low Level Design Fundamentals
Identity Map
Identity Map - Preventing Duplicate Objects
The Identity Map ensures that within a single unit of work, each database row is represented by exactly one in-memory object. The intent: when you load the same entity twice, you get the same instance back - not two independent copies that can drift apart. It does this by keeping a map of identity-to-object; on every load, it checks the map first and returns the existing object if present, only fetching and constructing a new one on a miss. Without it, the same logical entity can exist as several divergent objects, and the last one saved silently clobbers the others.
The Identity Map is the quiet collaborator behind the Unit of Work (Topic 20.3, 30.2) and the Repository (30.1). It's why, in a JPA EntityManager, em.find(Order.class, 42) called twice returns the identical reference. Most engineers rely on this behavior without naming it - and write bugs when they don't realize it's there, or assume it extends further than it does (it's scoped to one persistence context, not across them).
In interviews, the Identity Map rarely comes up by name, but the bug it prevents is a classic: "two parts of my code loaded the same order, each changed a different field, and one change vanished." Recognizing that as a missing-identity-map (lost-update) problem and naming the pattern is a precise, senior-level diagnosis.
The Problem It Solves
Without an identity map, two loads of the same row produce two objects:
Order a = orders.findById(orderId); // object A, loaded from row 42
a.applyDiscount(tenPercent);
Order b = orders.findById(orderId); // object B - a SECOND object for row 42
b.addNote("VIP customer");
orders.save(a); // writes A's state (discount, no note)
orders.save(b); // writes B's state (note, NO discount) - discount lost!
a and b are different objects representing the same row. Each saw a stale view of the other's change. Saving b overwrites the discount applied to a. This is a lost update, and it's insidious because each line looks correct in isolation. The same row has two representatives that don't know about each other.
How It Works
graph TD
A["findById(42)"] --> B{In identity map?}
B -->|hit| C[Return existing instance]
B -->|miss| D[Query DB, construct object]
D --> E[Put in identity map]
E --> C
F["findById(42) again"] --> B
style E fill:#1e1b4b,color:#fff
A flow diagram of an identity map. When findById is called for entity 42, the unit of work first checks whether that identity is already in the identity map. On a hit, it returns the existing instance immediately, with no database query. On a miss, it queries the database, constructs the object, places it in the identity map, and then returns it. A second findById for the same identity hits the map and returns the very same instance. The result is that one identity always maps to one in-memory object for the lifetime of the unit of work.
A sketch of the core:
public class IdentityMap {
private final Map<Class<?>, Map<Object, Object>> map = new HashMap<>();
@SuppressWarnings("unchecked")
public <T> T getOrLoad(Class<T> type, Object id, Supplier<T> loader) {
Map<Object, Object> byId = map.computeIfAbsent(type, k -> new HashMap<>());
return (T) byId.computeIfAbsent(id, k -> loader.get());
}
}
The map is scoped to the unit of work and discarded when it ends. This scoping is essential: a long-lived, global identity map would be a memory leak and would serve stale objects across transactions.
Scope Is the Subtlety
The Identity Map's guarantee holds within one persistence context, not across them. Two concurrent transactions each have their own identity map and their own copy of row 42 - so the identity map prevents lost updates within a transaction but does nothing about concurrent transactions racing on the same row. That cross-transaction problem is what optimistic and pessimistic concurrency (Topic 30.8) exist to solve. Confusing the two scopes is a common error: the identity map is intra-transaction; concurrency control is inter-transaction.
It also interacts with caching: a second-level cache extends identity-like behavior across transactions, but with its own staleness and invalidation concerns that are distinct from the per-context identity map.
Why This Matters in Interviews
The Identity Map is best deployed in interviews as a diagnosis. When a lost-update scenario is described - two code paths load and modify the same entity, one change disappears - naming "they got two objects for one row; an identity map (or proper unit-of-work scoping) gives one instance" is a sharp answer. Following it by clarifying that this is the intra-transaction fix, and that concurrent transactions still need optimistic/pessimistic locking, shows you understand exactly where each mechanism applies.
Common Mistakes
- Assuming two loads give two objects (or one) without knowing which. Code correctness depends on it; know your ORM's behavior.
- Confusing identity-map scope with concurrency control. It prevents lost updates within one transaction, not across concurrent ones.
- Global / long-lived identity maps. Memory leaks and stale objects served across transactions.
- Bypassing it with raw queries. A native query that constructs new objects can reintroduce duplicates the identity map would have unified.
Try It
In a JPA context, load the same entity twice with find and assert a == b (reference equality) - confirm the identity map is giving you one instance. Then construct the lost-update scenario deliberately: in two separate persistence contexts, load the same row, change different fields, and save both; watch one change vanish. The first experiment shows the identity map working within a context; the second shows its limit - and motivates the concurrency control of Topic 30.8.