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.

OperationAtomicity
writeEntityCAS-with-retry per normalized key
addListItemSingle Lua script
addIndexedListItemSingle Lua script (includes trim and back-index)
invalidateEntitySingle Lua script (cascade invalidation)