Caching Overview
Architecture and design of the Trovella caching layer — Upstash Redis, the @repo/cache wrapper, and how caching fits into the request lifecycle.
Role in the System
Trovella uses Upstash Redis as its caching layer. Redis serves two primary purposes today:
- Cache-aside reads — expensive or repeated query results are cached with a TTL to reduce database load.
- Health signaling — the
/api/healthendpoint pings Redis as one of three infrastructure health checks (alongside Cloud SQL and Typesense).
Future uses (planned but not yet implemented) include rate limiting at the API layer and session token caching.
The @repo/cache Package
All Redis access flows through the @repo/cache wrapper package. No other package in the monorepo may import ioredis directly — this is enforced by ESLint no-restricted-imports rules defined in packages/config-eslint/restrictions.js.
The package sits in the infra layer of the dependency hierarchy:
app (apps/web)
-> service (@repo/api, @repo/mcp)
-> infra (@repo/cache, @repo/ai, @repo/auth, @repo/logger, @repo/search)
-> core (@repo/db)
-> leaf (@repo/utils, @repo/validators, @repo/design-tokens)
Exports
| Export | Purpose |
|---|---|
cacheGet<T> | Cache-aside wrapper — returns cached value or calls fetcher |
cacheInvalidate | Delete a single cache key |
cacheInvalidatePattern | Delete all keys matching a glob pattern via SCAN |
getRedis | Access the singleton IORedis client |
disconnectRedis | Graceful shutdown — closes connection, nulls singleton |
checkRedis | Health check — returns { ok, latencyMs } |
Dependencies
The package depends on a single runtime library:
- ioredis
^5.10.1(via pnpm catalog)
Connection Model
The Redis client is a lazy singleton. The first call to getRedis() creates an IORedis instance from the REDIS_URL environment variable. Subsequent calls return the same instance. The client is configured with:
maxRetriesPerRequest: 3— automatically retries failed commands up to three timeslazyConnect: true— the TCP connection is not established until the first command
Calling disconnectRedis() sends a QUIT command and nulls the singleton. The next getRedis() call creates a fresh connection, which means the application can recover from transient Redis outages without a restart.
Environment Configuration
| Variable | Local (Docker Compose) | Production |
|---|---|---|
REDIS_URL | redis://localhost:6379 | Upstash Redis connection URL |
Local development uses a redis:8-alpine container defined in docker-compose.yml with a persistent redis-data volume. Production uses Upstash Redis, with the connection URL stored as upstash-redis-url in GCP Secret Manager. See Infrastructure — Cloud Resources for provisioning details.
What to Read Next
- Cache-Aside Pattern — how to use
cacheGetin your code - Key Conventions — naming rules and tenant scoping
- Invalidation — single-key and pattern-based cache clearing
- Health and Observability — monitoring Redis from the health endpoint