# Identity Map Pattern

## Problem

When multiple parts of an application load the same database record, they may create separate object instances for the same entity. This leads to inconsistencies: updating one instance doesn't update the other. It also wastes memory and database round-trips — the same row is fetched repeatedly.

## Solution

Maintain an in-memory map keyed by entity identity (typically the primary key). Before loading from the database, check the map. If the entity is already loaded, return the existing instance. On save, update the map. This ensures each entity exists at most once in memory per session.

## When to Use

- ORM or Unit of Work implementations
- When the same entity is accessed from multiple code paths in a single request
- To prevent duplicate database queries for the same record
- When object identity matters (same DB row = same object reference)

## When to Avoid

- Stateless APIs where each request starts fresh
- Read-only queries where duplicates don't matter
- Very large datasets where caching everything in memory is impractical
- Simple scripts with sequential, non-overlapping access patterns

## Pseudocode

```
class IdentityMap:
    cache = {}    # key: (type, id) → value: entity

    function get(type, id):
        return cache.get((type, id))

    function put(type, id, entity):
        cache[(type, id)] = entity

    function has(type, id):
        return (type, id) in cache
```

## Python Implementation

```python
from dataclasses import dataclass, field
from typing import Optional, Any


@dataclass
class User:
    id: int
    name: str
    email: str


class IdentityMap:
    def __init__(self):
        self._cache: dict[tuple[type, Any], Any] = {}
        self._hit_count = 0
        self._miss_count = 0

    def get(self, entity_type: type, entity_id: Any) -> Optional[Any]:
        key = (entity_type, entity_id)
        if key in self._cache:
            self._hit_count += 1
            return self._cache[key]
        self._miss_count += 1
        return None

    def put(self, entity_type: type, entity_id: Any, entity: Any) -> None:
        self._cache[(entity_type, entity_id)] = entity

    def remove(self, entity_type: type, entity_id: Any) -> None:
        self._cache.pop((entity_type, entity_id), None)

    def clear(self):
        self._cache.clear()
        self._hit_count = 0
        self._miss_count = 0

    @property
    def stats(self):
        return {"hits": self._hit_count, "misses": self._miss_count, "size": len(self._cache)}


class UserRepository:
    """Repository with identity map — simulates DB with a dict."""

    def __init__(self, identity_map: IdentityMap):
        self.identity_map = identity_map
        self.db_queries = 0
        # Simulated database
        self._db = {
            1: {"id": 1, "name": "Alice", "email": "alice@example.com"},
            2: {"id": 2, "name": "Bob", "email": "bob@example.com"},
            3: {"id": 3, "name": "Charlie", "email": "charlie@example.com"},
        }

    def find_by_id(self, user_id: int) -> Optional[User]:
        # Check identity map first
        cached = self.identity_map.get(User, user_id)
        if cached is not None:
            print(f"  [IdentityMap] HIT for User#{user_id}")
            return cached

        # Simulate DB query
        print(f"  [DB] SELECT * FROM users WHERE id = {user_id}")
        self.db_queries += 1
        row = self._db.get(user_id)
        if row is None:
            return None

        user = User(**row)
        self.identity_map.put(User, user_id, user)
        return user


# --- Demo ---
identity_map = IdentityMap()
repo = UserRepository(identity_map)

print("=== First load (cache miss, hits DB) ===")
user1 = repo.find_by_id(1)
print(f"  Got: {user1}")

print()
print("=== Second load same ID (cache hit, no DB) ===")
user1_again = repo.find_by_id(1)
print(f"  Got: {user1_again}")
print(f"  Same object? {user1 is user1_again}")

print()
print("=== Load different user (cache miss) ===")
user2 = repo.find_by_id(2)
print(f"  Got: {user2}")

print()
print("=== Mutate user1 — reflected everywhere ===")
user1.email = "alice@newdomain.com"
user1_check = repo.find_by_id(1)
print(f"  user1.email = {user1_check.email}")

print()
print(f"=== Stats ===")
print(f"  DB queries: {repo.db_queries}")
print(f"  Identity map: {identity_map.stats}")
```

**Output:**
```
=== First load (cache miss, hits DB) ===
  [DB] SELECT * FROM users WHERE id = 1
  Got: User(id=1, name='Alice', email='alice@example.com')

=== Second load same ID (cache hit, no DB) ===
  [IdentityMap] HIT for User#1
  Got: User(id=1, name='Alice', email='alice@example.com')
  Same object? True

=== Load different user (cache miss) ===
  [DB] SELECT * FROM users WHERE id = 2
  Got: User(id=2, name='Bob', email='bob@example.com')

=== Mutate user1 — reflected everywhere ===
  [IdentityMap] HIT for User#1
  user1.email = alice@newdomain.com

=== Stats ===
  DB queries: 2
  Identity map: {'hits': 2, 'misses': 2, 'size': 2}
```

## JavaScript Implementation

```javascript
class IdentityMap {
  constructor() {
    this._cache = new Map();
    this.hits = 0;
    this.misses = 0;
  }

  _key(type, id) {
    return `${type}:${id}`;
  }

  get(type, id) {
    const key = this._key(type, id);
    if (this._cache.has(key)) {
      this.hits++;
      return this._cache.get(key);
    }
    this.misses++;
    return null;
  }

  put(type, id, entity) {
    this._cache.set(this._key(type, id), entity);
  }

  get stats() {
    return { hits: this.hits, misses: this.misses, size: this._cache.size };
  }
}

class UserRepository {
  constructor(identityMap) {
    this.identityMap = identityMap;
    this.dbQueries = 0;
    this._db = new Map([
      [1, { id: 1, name: "Alice", email: "alice@example.com" }],
      [2, { id: 2, name: "Bob", email: "bob@example.com" }],
    ]);
  }

  findById(id) {
    const cached = this.identityMap.get("User", id);
    if (cached) {
      console.log(`  [IdentityMap] HIT for User#${id}`);
      return cached;
    }
    console.log(`  [DB] SELECT * FROM users WHERE id = ${id}`);
    this.dbQueries++;
    const row = this._db.get(id);
    if (!row) return null;

    const user = { ...row }; // Shallow copy from "DB"
    this.identityMap.put("User", id, user);
    return user;
  }
}

// --- Demo ---
const im = new IdentityMap();
const repo = new UserRepository(im);

console.log("=== First load (cache miss) ===");
const user1 = repo.findById(1);
console.log(`  Got: User(id=${user1.id}, name=${user1.name}, email=${user1.email})`);

console.log("\n=== Second load same ID (cache hit) ===");
const user1Again = repo.findById(1);
console.log(`  Same reference? ${user1 === user1Again}`);

console.log("\n=== Mutate — reflected everywhere ===");
user1.email = "alice@newdomain.com";
const check = repo.findById(1);
console.log(`  email after mutation: ${check.email}`);

console.log(`\nDB queries: ${repo.dbQueries}`);
console.log("Identity map stats:", im.stats);
```

## Related Patterns

- **Repository** — the identity map lives inside the repository, ensuring that `find(id)` always returns the same object instance.
- **Unit of Work** — the unit of work uses the identity map to track which objects have been modified during a transaction.
- **Flyweight** — flyweight shares immutable intrinsic state; identity map shares mutable entity instances by identity.
