This document describes the Redis caching layer implementation for the Credence Backend.
The caching layer provides a generic Redis-based caching service with:
- Connection management - Singleton Redis client with health monitoring
- Namespacing - Automatic key namespacing (e.g.,
trust:score:0x123) - TTL support - Set expiration times on cached values
- Type safety - Full TypeScript support with JSDoc documentation
- Error handling - Graceful fallback when Redis is unavailable
- Health checks - Built-in Redis health monitoring
Singleton Redis client that manages the connection lifecycle:
import { redisConnection } from '../cache/redis.js'
// Auto-connects on first use
await redisConnection.connect()
// Health check
const healthy = await redisConnection.isHealthy()
// Graceful shutdown
await redisConnection.disconnect()High-level caching interface with namespacing and TTL:
import { cache } from '../cache/redis.js'
// Store data with TTL
await cache.set('trust', 'score:0x123', { score: 85 }, 300)
// Retrieve data (auto-parses JSON)
const score = await cache.get('trust', 'score:0x123')
// Delete data
await cache.delete('trust', 'score:0x123')
// Health check
const { healthy, error } = await cache.healthCheck()Retrieve a cached value. Automatically parses JSON strings.
Parameters:
namespace- Cache namespace (e.g., 'trust', 'bond')key- Key within namespace
Returns: Parsed value or null if not found
Read-through cache with cache-stampede protection via SingleFlight coalescing. When multiple concurrent callers miss the cache for the same (namespace, key), only one origin call is made; all others transparently wait for the same result.
Parameters:
namespace- Cache namespacekey- Key within namespacefetchFn- Origin fetch function called on cache missttl- Time-to-live in seconds for the cached value
Stampede-protection details:
- Fast path: checks the cache — if hit, returns immediately.
- Acquires a SingleFlight slot keyed to
(namespace, key). - Double-checks the cache after acquiring the slot (another caller may have populated it while we waited).
- Calls
fetchFnonly if still a miss; stores the result in cache. - All waiters share the same resolved value (or error).
Returns: The cached or freshly-fetched value.
Example:
import { cache } from '../cache/redis.js'
const bond = await cache.getOrFetch(
'bond',
'id:42',
() => repository.findById(42),
300,
)Store a value in cache. Automatically JSON-serializes objects.
Parameters:
namespace- Cache namespacekey- Key within namespacevalue- Value to cache (string or object)ttl- Optional time-to-live in seconds
Returns: true if successful, false on error
Delete a cached value.
Returns: true if key existed and was deleted
Delete all keys in a namespace.
Returns: Number of keys deleted
Check if a key exists.
Returns: true if key exists
Set TTL for an existing key.
Returns: true if TTL was set
Get remaining TTL for a key.
Returns:
> 0- Remaining seconds-1- Key exists but has no expiry-2- Key doesn't exist
Check Redis connection health.
Returns: Health status with optional error message
The cache automatically namespaces keys to prevent collisions:
trust:score:0x123 -> Trust score for address
bond:status:0x123 -> Bond status for address
api:response:users -> API response cache
Recommended namespaces:
trust- Trust scores and reputation databond- Bond status and amountsapi- API response cachingsession- User session datarate-limit- Rate limiting data
Recommended TTL values by data type:
| Data Type | TTL | Reason |
|---|---|---|
| Trust scores | 5-15 minutes | Balance freshness with performance |
| Bond status | 1-5 minutes | Critical data, shorter cache |
| API responses | 1-60 minutes | Varies by endpoint |
| Rate limits | 1 hour | Fixed window |
| Sessions | 24 hours | User session duration |
The cache service is designed to be resilient:
- Connection failures - Methods return
null/falseinstead of throwing - Redis errors - Logged and gracefully handled
- JSON parsing - Falls back to string values if parsing fails
- Health checks - Use
healthCheck()to verify Redis status
// Example: Fallback pattern
const cached = await cache.get('trust', 'score:0x123')
if (cached === null) {
// Cache miss or Redis unavailable
const fresh = await computeTrustScore('0x123')
await cache.set('trust', 'score:0x123', fresh, 300)
return fresh
}
return cachedRequired Redis configuration:
# Redis connection URL
REDIS_URL=redis://localhost:6379
# Optional: Custom Redis settings
REDIS_CONNECT_TIMEOUT=5000The cache layer includes comprehensive tests:
# Run all cache tests
npm test src/cache/__tests__
# Run with coverage
npm run test:coverageTests cover:
- Connection management
- Cache operations (get/set/delete)
- TTL handling
- Namespacing
- Error scenarios
- Health checks
- Connection pooling - Singleton client manages connection efficiently
- Batch operations - Use
clearNamespace()for bulk deletions - Memory usage - Set appropriate TTLs to prevent memory bloat
- Network latency - Cache frequently accessed data
- JSON serialization - Avoid caching very large objects
Monitor Redis health and performance:
// Health check endpoint
app.get('/api/health/cache', async (req, res) => {
const { healthy, error } = await cache.healthCheck()
res.json({
cache: {
healthy,
error: error || undefined
}
})
})Key metrics to monitor:
- Connection success rate
- Cache hit/miss ratios
- Memory usage
- Response times
- Error rates
- Network isolation - Keep Redis in private networks
- Authentication - Use Redis AUTH in production
- TLS encryption - Enable Redis TLS for sensitive data
- Key naming - Avoid sensitive data in cache keys
- Data sanitization - Validate data before caching
A cache stampede (thundering herd) occurs when many concurrent requests
miss the cache for the same key simultaneously, each triggering an expensive
origin call. The getOrFetch method uses the SingleFlight pattern to
prevent this.
The SingleFlight class (src/lib/singleflight.ts) guarantees that for a
given deduplication key, only one async function executes at a time.
If a second caller arrives while the first is still in-flight, it
piggybacks on the same promise instead of starting a duplicate call.
Request A ──→ cache miss ──→ acquires slot ──→ fetchFn() ──→ all get result
Request B ──→ cache miss ──→ waits on A ──────────────────→ all get result
Request C ──→ cache miss ──→ waits on A ──────────────────→ all get result
(only 1 origin call)
Inside the SingleFlight slot, getOrFetch re-checks the cache before
calling the origin (fetchFn). This handles the edge case where two
concurrent callers both miss the cache, but a previous SingleFlight
call already populated it by the time the waiter acquires the slot.
- Expensive or slow origin calls (DB queries, external API calls, complex computations)
- High-read, low-write data accessed by multiple concurrent handlers
- Any cache hot path where a miss triggers a noticeable load spike
The SingleFlight primitive can also be used standalone for any
problem that needs request coalescing:
import { singleflight } from '../lib/singleflight.js'
const result = await singleflight.do('my-operation-key', async () => {
return await expensiveWork()
})- Always set TTL - Prevent memory leaks
- Use namespaces - Avoid key collisions
- Handle failures - Always check return values
- Monitor health - Use health checks in production
- Test failures - Verify graceful degradation
- Document TTLs - Clear cache invalidation strategy
- Size limits - Avoid caching very large objects
- Consistent patterns - Standardize key naming