Caching strategy
Evolve applies caching at multiple layers: the Next.js frontend, the GraphQL gateway, and the backend domain services. Each layer has a different caching strategy suited to its role.
Next.js frontend
HTTP caching
The frontend sets Cache-Control headers for unauthenticated requests:
s-maxage=900; stale-while-revalidate=2592000
This caches pages on the CDN for 15 minutes and serves stale responses for up to 30 days while revalidating in the background. Authenticated requests (users with a session token) bypass this cache entirely.
Data fetching
Server-side GraphQL requests use Next.js Incremental Static Regeneration (ISR) with a 15-minute revalidation interval:
const data = await serverClient(GetProductDetailPage, variables, {
cache: authHeaders ? "no-cache" : "force-cache",
next: {
revalidate: 900,
tags: ["product_detail_page", slug],
},
});
- Unauthenticated: responses are cached with
force-cacheand revalidated every 15 minutes - Authenticated: responses use
no-cacheto ensure customer- specific data is always fresh - Draft mode: all caching is disabled for content preview
React's cache() function deduplicates identical requests within a
single server render, preventing multiple fetches for the same data
during one page load.
On-demand revalidation
Cached pages can be revalidated immediately using Next.js cache tags. This is useful for content updates that should be visible before the 15-minute TTL expires (e.g., a CMS publish event triggering revalidation of affected pages).
GraphQL gateway
The gateway uses Apollo Server with an LRU cache backed by Keyv. In production, the cache store connects to Redis. In development, it falls back to an in-memory LRU cache.
Automatic Persisted Queries (APQ) are enabled between the gateway and all subgraph services. This reduces bandwidth by sending only the query hash after the first request, instead of the full query text.
Backend services
Redis caching
Backend services use the framework's cache module, which provides a Redis-backed cache with automatic fallback to a no-op store when Redis is unavailable:
import { cache } from "@evolve-framework/core/cache";
await cache.configure(config.REDIS_URL);
const result = await cache.wrapFn({
key: `product:${storeContext.cacheKeyPrefix}:${slug}`,
fn: () => fetchProduct(slug),
ttl: 300_000, // 5 minutes
});
The wrapFn method handles cache lookup, miss execution, and storage
in a single call with built-in tracing.
DataLoader caching
DataLoaders that fetch frequently accessed data (categories, breadcrumbs, content snippets) are configured with Redis-backed caching. Cache keys include the store context (store key, locale, currency) to prevent data leaking between stores:
new DataLoaderCache(fetchFn, {
cache: {
ttl: 5 * MINUTES,
storeFn: () => cache.getStore("cms:dataloaders"),
},
cacheKeyFn: (key) =>
`snippet:${storeContext.cacheKeyPrefix}:${key}`,
});
Cache namespaces
Services use namespaced cache stores to keep cached data separated:
| Namespace | Service | Purpose |
|---|---|---|
graphql-gateway:apollo-cache | Gateway | Apollo Server query cache |
cms:page-cache | CMS | Page content cache |
cms:dataloaders | CMS | DataLoader result cache |
cms:cache | CMS | CMS version tracking |
Guidelines
- Default to no caching: add caching explicitly where it provides measurable benefit. Premature caching creates staleness bugs that are hard to debug.
- Include the store context in cache keys: use the
storeContext.cacheKeyPrefix(which combines store key, locale, and currency) to prevent cross-store cache pollution. - Use short TTLs for commerce data: product availability and pricing change frequently. 5 minutes is a reasonable default for catalog data.
- Use longer TTLs for CMS content: content changes less frequently. 15 to 60 minutes works well for page content and navigation.
- Skip caching for customer data: carts, orders, and account information should never be cached in shared caches.