Skip to main content

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-cache and revalidated every 15 minutes
  • Authenticated: responses use no-cache to 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:

NamespaceServicePurpose
graphql-gateway:apollo-cacheGatewayApollo Server query cache
cms:page-cacheCMSPage content cache
cms:dataloadersCMSDataLoader result cache
cms:cacheCMSCMS 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.