What is an Identity Map?
An identity map is a design pattern that ensures only one instance of any given object exists in memory at a time. When you query for the same entity multiple times, you get the same Python object reference rather than different instances with the same ID. Without identity map:Benefits
- No stale data - All references to an entity stay synchronized
- Memory efficiency - One object in memory instead of many duplicates
- Relationship consistency - Related entities always point to cached instances
- Reduced network requests - Cached objects avoid redundant queries
How It Works: Wrap Validator Flow
Implementation: Pydantic Wrap Validators
Most libraries implement identity maps as a separate layer (like SQLAlchemy’sSession.identity_map or Apollo’s InMemoryCache). This library integrates caching directly into Pydantic model construction using wrap validators.
Why Wrap Validators?
Pydantic v2 introduced@model_validator(mode='wrap'), which gives complete control over object construction. A wrap validator:
- Receives raw input data before Pydantic processes it
- Can return a cached instance instead of constructing a new one
- Can pre-process data before passing to Pydantic’s default handler
- Executes before field validation - extremely efficient
StashObject Base Class
All entity types inherit fromStashObject, which implements the wrap validator:
Nested Cache Lookups
Before Pydantic validates data, the wrap validator processes nested objects to replace dictionaries with cached instances:Field Merging on Cache Hits
When returning a cached instance, the validator merges new fields from the GraphQL response. This logic is inlined in_identity_map_validator (no separate method):
StashEntityStore Implementation
TheStashEntityStore class manages the actual cache storage:
Cache Structure
StashContext, which creates the store and wires it to StashObject as part of client initialization, then un-wires it on close:
StashEntityStore.__init__ deliberately does not set StashObject._store so that the store can be constructed without global side effects. StashContext owns the wiring because it also owns the cleanup.
Key design decisions:
-
Cache key format:
(type_name, entity_id)tuples- Avoids collisions between different types
- Simple and fast lookup
-
TTL using
time.monotonic():- Immune to system clock changes
- Optional (None = never expire)
- Per-store configuration (not per-entry)
-
Thread-safe with
RLock:- Allows recursive locking (same thread can acquire multiple times)
- Protects cache dict modifications
- Lock released before calling user code
Cache Operations
Comparison to Other Implementations
vs SQLAlchemy Session
vs Apollo Client InMemoryCache
Advantages of Wrap Validator Approach
- Earlier caching - Before any validation work
- Transparent - No separate cache API to learn
- Nested objects - Automatically use cached instances
- Type-safe - Still full Pydantic validation when needed
- Simple - Just use
.from_dict(), caching happens automatically
Trade-offs
- Class-level store - All instances share one store (not session-per-context)
- No partial object merging - Can’t merge at field level (only full fields)
- Python-specific - Wrap validators are Pydantic v2 feature
- Memory usage - Keeps objects in memory (no weak references)
Performance Characteristics
Cache Hit Path
Cache Miss Path
Memory Usage
- Per cached entity: Size of Python object + CacheEntry overhead (~100 bytes)
- Per cache entry: Tuple key (48 bytes) + CacheEntry (48 bytes) = ~96 bytes
- Total: Object size + ~200 bytes overhead per cached entity
Best Practices
When to Use Identity Map
✅ Use identity map when:- Making multiple queries for the same entities
- Working with entity relationships
- Need consistency across references
- Building long-running applications
- One-off queries (just use client directly)
- Short-lived scripts
- Memory constrained environments
- Need isolation between operations
TTL Configuration
Manual Cache Management
Implementation Files
stash_graphql_client/types/base.py- StashObject with_identity_map_validatorwrap validatorstash_graphql_client/store.py- StashEntityStore implementationstash_graphql_client/types/unset.py- UnsetType for three-state fields
Next Steps
- Library Comparisons - Detailed comparison with alternatives
- Bidirectional Relationships - How relationships work
- Usage Patterns - Practical examples
- API Reference - Complete StashEntityStore API