Core Concepts
Understand the fundamental concepts behind redis-graph-cache.
This library treats your Redis store as a normalized data graph
With three kinds of registered schemas: Entity, List, and Indexed List
1. Entity
A single addressable object with optional fields and one-to-one / one-to-many relations to other entities. Stored as one Redis key.
const post = {
type: 'entity' as const,
id: 'id',
key: (id: string) => `post:${id}`,
fields: {
title: { type: 'string' as const },
content: { type: 'string' as const },
},
relations: {
author: { type: 'user', kind: 'one' as const },
comments: { type: 'comment', kind: 'many' as const },
},
ttl: 3600,
};Key characteristics
- Each entity has a unique ID and Redis key
- Nested objects are split into their own entity keys
- Relations are stored as ID references
- Supports one-to-one and one-to-many relationships
2. List
A collection of entity IDs stored as a single JSON array string under one Redis key. Best for short, flat collections.
const recentPosts = {
type: 'list' as const,
entityType: 'post',
key: () => 'posts:recent',
idField: 'id',
ttl: 600,
};Trade-offs
Pros
- Minimal storage overhead
- Simple Lua scripts
- Atomic add/remove operations
Cons
- Every read of any page parses the whole array
- Not suitable for lists with thousands of items
- No built-in pagination or sorting
3. Indexed List
A collection of entity IDs stored as a Redis ZSET (sorted set), scored by any field on the entity. Enables paginated reads, sorting, size capping, and cascade invalidation.
const globalFeed = {
type: 'indexedList' as const,
entityType: 'post',
key: () => 'feed:global',
idField: 'id',
scoreField: 'createdAt',
maxSize: 10_000,
trackMembership: true,
ttl: 86400,
};Features
- Paginated reads: Efficient pagination with offset and limit
- Sorting: Sorted by any numeric or timestamp field
- Size capping: Automatically trim to maxSize on insert
- Cascade invalidation: Remove entities from all tracked lists atomically
- Score derivation: Uses entity field or insertion timestamp
4. Normalization
When you write an entity that contains nested relations, the cache normalizes the input: each relation becomes its own Redis key, with the parent storing references via internal fields.
// Input: Post with embedded author
{
id: 1,
title: 'Hello World',
author: { id: 9, name: 'Ada' }
}
// Stored in Redis:
// post:1 → { id: 1, title: 'Hello World', __rse_authorId: 9 }
// user:9 → { id: 9, name: 'Ada' }Benefits of normalization
- No duplication: Each entity exists once in Redis
- Easy updates: Update author once → every post sees it
- Efficient storage: Shared data isn't duplicated across keys
- Consistency: Single source of truth for each entity
5. Hydration
When you read an entity, the cache hydrates it by fetching the entity and walking every relation to reconstruct the full nested object graph.
// Read post:1
const post = await cache.readEntity('post', 1);
// Hydration process:
// 1. Fetch post:1 → { id: 1, title: 'Hello World', __rse_authorId: 9 }
// 2. Fetch user:9 → { id: 9, name: 'Ada' }
// 3. Merge: { id: 1, title: 'Hello World', author: { id: 9, name: 'Ada' } }Hydration options
- maxDepth: Limit recursion depth (default: 5)
- selectiveFields: Only return specific fields
- excludeRelations: Skip specific relation names
- memoryLimit: Cap on per-request memory usage
Circular reference handling
If hydration revisits an entity it's currently traversing, the cache emits a stub like { id: <id>, $ref: '<entityType>:<id>' } instead of dropping the reference silently. The $ref field is a public marker that callers can detect to break recursion themselves.
6. Atomicity
All read-modify-write paths are protected by atomic Lua scripts, so concurrent writes converge correctly without client-side locks.
| Operation | Atomicity |
|---|---|
writeEntity | CAS-with-retry per normalized key |
addListItem | Single Lua script |
addIndexedListItem | Single Lua script (includes trim and back-index) |
invalidateEntity | Single Lua script (cascade invalidation) |