d9e5668cec
Fixes and improvements from exhaustive audit: Consistency fixes in SKILL.md: - 'Pipe & Filter' → 'Pipe and Filter' (one stray '&' found and corrected) - 'Singleton for factory instance' → clarified to 'register factory as singleton-scoped via DI container' (less misleading wording) - Documentation Format section updated with note that SKILL.md itself is the authoritative source for related-pattern combinations Coverage fix — Related Patterns sections: - Added '## Related Patterns' to ALL 94 pattern files (was 0/94) - Each section lists 3–6 related patterns with relationship descriptions - Covers: why they're related, when to prefer one vs the other, and which are often confused SOLID principles → Creational → Structural → Behavioral → Architectural → Concurrency → Functional → Resilience → Data Access → Messaging → Testing → Error Handling → Microservice — all 13 categories covered Code verification: - Python: 0 failures (all 85 testable blocks pass) - Go: 0 failures (all 76 testable blocks pass) - JavaScript: 0 failures (all 78 testable blocks pass) - All 239 code blocks verified correct after edits Final skill state: - 108 files, 36,524 lines across 13 reference categories - 94/94 pattern files have Related Patterns sections - 2,815-line SKILL.md with 67 decision trees, 23 scenarios, 0 broken references, 0 naming inconsistencies
7.2 KiB
7.2 KiB
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
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
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.