Skip to main content

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:
With identity map:

Benefits

  1. No stale data - All references to an entity stay synchronized
  2. Memory efficiency - One object in memory instead of many duplicates
  3. Relationship consistency - Related entities always point to cached instances
  4. 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’s Session.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:
  1. Receives raw input data before Pydantic processes it
  2. Can return a cached instance instead of constructing a new one
  3. Can pre-process data before passing to Pydantic’s default handler
  4. Executes before field validation - extremely efficient
This means cache lookup happens before any validation or deserialization work.

StashObject Base Class

All entity types inherit from StashObject, which implements the wrap validator:

Nested Cache Lookups

Before Pydantic validates data, the wrap validator processes nested objects to replace dictionaries with cached instances:
Why this matters:

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):
Why field merging?

StashEntityStore Implementation

The StashEntityStore class manages the actual cache storage:

Cache Structure

The identity map is activated by 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:
  1. Cache key format: (type_name, entity_id) tuples
    • Avoids collisions between different types
    • Simple and fast lookup
  2. TTL using time.monotonic():
    • Immune to system clock changes
    • Optional (None = never expire)
    • Per-store configuration (not per-entry)
  3. 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

  1. Earlier caching - Before any validation work
  2. Transparent - No separate cache API to learn
  3. Nested objects - Automatically use cached instances
  4. Type-safe - Still full Pydantic validation when needed
  5. Simple - Just use .from_dict(), caching happens automatically

Trade-offs

  1. Class-level store - All instances share one store (not session-per-context)
  2. No partial object merging - Can’t merge at field level (only full fields)
  3. Python-specific - Wrap validators are Pydantic v2 feature
  4. 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
Example: 1000 cached scenes with 20 fields each ≈ 1-2 MB

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
Skip identity map when:
  • 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_validator wrap validator
  • stash_graphql_client/store.py - StashEntityStore implementation
  • stash_graphql_client/types/unset.py - UnsetType for three-state fields

Next Steps