LazyLayersv0.5.3

Node.js cache quickstart

Install a TypeScript cache npm package, start with in-memory L1, then add Redis L2, read-through loading, and invalidation.

Install the lazy-layers-cache npm package to start with a runnable in-memory L1 cache, then connect the same API to Redis L2 when multiple Node.js instances need to share data. You need Node.js 20 or 22+. The first example needs no database or Redis.

Install the package

npm install lazy-layers-cache

Run your first cached read

Save this file and run node cache-demo.mjs.

cache-demo.mjs
import { setupCache } from 'lazy-layers-cache'

const cache = await setupCache({ namespace: 'demo', redis: false })
let loaderCalls = 0

try {
  const loadUser = async () => {
    loaderCalls++
    return { id: '42', name: 'Ada' }
  }

  await cache.getOrSet('user:42', loadUser)
  const user = await cache.getOrSet('user:42', loadUser)
  console.log(user.name, loaderCalls) // Ada 1

  await cache.invalidate('user:42')
  await cache.getOrSet('user:42', loadUser)
  console.log(loaderCalls) // 2
} finally {
  await cache.close()
}

The first read runs the loader. The second reuses the value. Invalidating the key makes the next read load it again.

Share values across instances

Set REDIS_URL to your Redis 6+ service. Create one cache per application process:

src/cache/index.js
import { setupCache } from 'lazy-layers-cache'

export const cache = await setupCache({
  namespace: 'users-api',
  redis: { required: true },
})

For the TypeScript example, define the value your application reads:

src/types/user.ts
export interface User {
  id: string
  name: string
}

Use the same namespace and Redis service for instances that share values. Setup waits for Redis and the event subscription to be ready. required: true prevents a missing URL from silently starting isolated L1 caches.

Connect your database reads and writes

The following repository assumes you provide src/db.js. Adapt the database calls to your client. Return undefined for a missing record, and pass the abort signal when your client supports cancellation.

src/users/repository.js
import { cache } from '../cache/index.js'
import { db } from '../db.js'

export function getUser(id) {
  return cache.getOrSet(`user:${id}`, async ({ signal } = {}) => {
    const user = await db.users.findById(id, { signal })
    return user ?? undefined
  })
}

export async function updateUser(id, patch) {
  const user = await db.users.update(id, patch)
  await cache.invalidate(`user:${id}`)
  return user
}

Commit the database write before invalidating. Invalidation clears cached state and publishes a delete event to peers. Event delivery is asynchronous, so it does not make the database write and every cached read atomic.

Close the cache on shutdown

After your server stops accepting requests and drains active work, call:

Application shutdown
await cache.close()

close() is idempotent. It disconnects the event bus, stops observability, and closes a Redis client created by setupCache. Your application closes any client it supplied itself.

What is already configured

In-flight dedupe shares concurrent same-key loads in one process. Redis per-key leases coordinate loads between instances. Loader timeouts, bounded origin work, stale fallback, negative caching, and circuit breakers are already active.

A successful loader can also prime peer L1 caches. setupCache limits the encoded broadcast to 32 KiB. Larger events skip priming, and memory pressure can prevent L1 admission. The configuration reference documents every default and override.

Redis or bus failures can degrade to origin reads. Your application must still handle loader errors, overload, and lock deadlines. See failure handling.

Add only what your application needs

NeedNext step
Warm a known key before trafficprewarm, using the same loader
Invalidate a key familyinvalidateByPattern
Tune memory, TTLs, and concurrencyProduction setup
Coordinate a durable payment or order attemptTransaction coordination, a separate API

Where to next

On this page