Skip to main content

Caching Architecture

The Charles Schwab NextJS Web Monorepo implements a comprehensive multi-layer caching strategy designed to optimize performance, reduce server load, and improve user experience. This document details the caching mechanisms, strategies, and tools used throughout the application ecosystem.

Overview

The caching architecture employs multiple layers of caching strategies across different components of the system:

Next.js Caching Configuration

App Router Cache Settings

The primary caching configuration is managed in Next.js configuration files:

next.config.js - Cache Configuration
const nextConfig = {
reactStrictMode: true,
experimental: {
useCache: true, // Enables Next.js experimental caching
},
// Additional cache-related configurations...
};

Page-Level Caching

Individual pages can specify their own revalidation intervals:

Page-level revalidation
// Next.js will invalidate the cache when a
// request comes in, at most once every 30 minutes (1800 seconds)
export const revalidate = 1800;
export const dynamicParams = true;
export const dynamic = 'force-static';

Cache Revalidation System

Revalidation API Architecture

The monorepo includes a sophisticated cache revalidation system that allows for targeted cache invalidation:

apps/www.schwab.com/src/app/api/revalidate/route.ts
export async function GET(request: NextRequest): Promise<Response> {
const path = request.nextUrl.searchParams.get('path');
const tags = request.nextUrl.searchParams.get('tags');
const type = request.nextUrl.searchParams.get('type');
const source = request.nextUrl.searchParams.get('source') ?? 'education';

try {
let results: TRevalidateResults | TRevalidateResults[] | unknown;
const ESource = convertToPascal(source);

if (type === 'redirects') {
revalidateCacheByTag(`redirects_${EDrupalSource[ESource].toLowerCase()}`);
results = await syncRedirectsToEdge(EDrupalSource[ESource]);
}

if (tags && tags.length > 0) {
results = revalidateCacheByTag(tags, EDrupalSource[ESource]);
}

if (path && path.length > 0) {
results = [] as TRevalidateResults[];
const flagPermutations = await generatePermutations(allFlags);

flagPermutations.forEach((code) => {
(results as TRevalidateResults[]).push(revalidateCacheByPath(`/${code}${path}`));
});
}

return Response.json({
results,
now: Date.now(),
});
} catch (error) {
return Response.json({ error: error.message }, { status: 500 });
}
}

Tag-Based Cache Invalidation

The system implements tag-based cache invalidation for precise control over cache purging:

packages/fetch/src/utilities/revalidateCacheByTag.ts
export function revalidateCacheByTag(tag: string | string[] | null, source?: EDrupalSource) {
let tags: string[];

if (isArrayWithData(tag)) {
tags = tag as string[];
} else {
tags = (tag as string).split(',');
}

tags.forEach((tagItem) => {
const tagName = !source || tagItem.includes(source) ? tagItem : `${source}_${tagItem}`;
revalidateTag(tagName);
});

return { status: 'revalidatedTags', tags };
}

Path-Based Cache Invalidation

Path-specific cache invalidation for targeted content updates:

packages/fetch/src/utilities/revalidateCacheByPath.ts
export function revalidateCacheByPath(path: string | null): TRevalidateResults {
if (path && path.length > 0) {
revalidatePath(path);
return { status: 'revalidatedPath', path };
}
return { status: 'invalidRequest', path: 'no-path-provided' };
}

Cache Tags Implementation

Component-Level Cache Tags

Components implement cache tags for granular cache management:

Branch locator cache tags example
const cacheTagList = [
`branches_${zipcode}`,
`branches_location_${city}_${state}`,
'branches_search_results',
'branches_global'
];

const response = await getBranchesFetch(queryParams, cacheTagList);

Content Management System Integration

Cache tags are integrated with Drupal CMS for content-driven invalidation:

CMS cache tags from mock data
{
"cacheTags": "paragraph:33326,paragraph:33331,media:12051,media:12056,media:12061,taxonomy_term:466,taxonomy_term:906,taxonomy_term:331,paragraph:33336,taxonomy_term:791,taxonomy_term:1151,taxonomy_term:491,taxonomy_term:511,taxonomy_term:1131,taxonomy_term:901,taxonomy_term:826,taxonomy_term:671,taxonomy_term:856,paragraph:986,media:8651,node:2966"
}

Edge Config Cache

Vercel Edge Config Integration

The system leverages Vercel Edge Config for distributed configuration caching:

Environment configuration
EDGE_CONFIG_SCHWAB_DATA_CACHE="https://edge-config.vercel.com/ecfg_kmiaazuadspy4hyjx4mmeam7vbge?token=7de67818-04d7-45b7-b06d-dd7d162a084c"

Redirect Cache Synchronization

Edge config is used to manage redirect caching and synchronization:

Redirect cache synchronization
export async function syncRedirectsToEdge(source: EDrupalSource.Education) {
try {
const ESource: EDrupalSource = EDrupalSource[source.toUpperCase()];
const redirectsFromDrupal = await getRedirectsFromDrupal(ESource);
const redirectsOnEdge = await getAllRedirectsFromEdge();
const edgeConfigPostData = redirectsFromDrupal.data ? redirectsFromDrupal.data : [];

// Search through all current redirects and identify which ones should be deleted
Object.entries(redirectsOnEdge).forEach(([key, value]) => {
const matchedRecord = edgeConfigPostData.find((record) => record.key === key);

if (matchedRecord === undefined && value.source === source) {
edgeConfigPostData.push({
key,
operation: 'delete',
});
}
});

if (redirectsFromDrupal.data) {
const postResult = await postRedirectsToEdge(redirectsFromDrupal.data);
return { postResult, postedData: redirectsFromDrupal };
}

return { error: 'No redirects returned from Drupal API.' };
} catch (error) {
SchLogger.error(`syncRedirectsToEdge Error: ${error.message}`);
}
}

API Route Caching

HTTP Cache Headers

API routes implement sophisticated HTTP caching headers:

Search suggest API caching
// Error response - no caching
return new Response(JSON.stringify(responseError), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
Pragma: 'no-cache',
Expires: '0',
},
});

// Success response - aggressive caching with stale-while-revalidate
return new Response(JSON.stringify(response), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=300, stale-while-revalidate=150',
},
});

Rate Data Caching

Financial data APIs implement specific caching strategies:

ARL rate caching strategy
// Error response - no caching
return new Response(JSON.stringify(responseError), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
Pragma: 'no-cache',
Expires: '0',
},
});

// Success response - 5-minute cache with stale-while-revalidate
return new Response(JSON.stringify(response.data), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=300, stale-while-revalidate=150',
},
});

Data Fetching Cache Strategy

Drupal Content Caching

Content fetched from Drupal CMS includes configurable revalidation times:

Drupal fetch revalidation configuration
DRUPAL_FETCH_REVALIDATE_TIME="31536000"  # 1 year in seconds

Cache Tag Association

Data fetching functions associate appropriate cache tags:

Branch data fetching with cache tags
const cacheTagList = [
`branches_${branchId}`,
'branches_detail',
'branches_global'
];

const branchData = await getBranchesFetch(queryParams, cacheTagList);

Browser Cache Optimization

Back-Forward Cache (bfcache)

The system includes specific optimizations for browser back-forward cache:

bfcache integrity configuration
{
"testName": "BFCACHE_INTEGRITY_REQUIRE_NOOPENER_ATTRIBUTE",
"description": "Requires that links opened with `window.open` use the `noopener` attribute to eliminate a source of eviction from the browser's Back-Forward Cache.",
"error": "When using `window.open()`, use `rel='noopener'` to avoid breaking bfcache integrity and reduce security risks."
}

Static Asset Caching

Next.js configuration includes static asset cache optimization:

Static asset cache headers
{
source: '/nextassets/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable'
}
]
}

Cache Monitoring and Analytics

Cache Performance Metrics

The system includes monitoring for cache performance through Vercel's Observability dashboard:

Vercel Observability Dashboard

Cache performance is monitored through Vercel's built-in observability tools:

Dashboard Location: https://vercel.com/charles-schwab/www.schwab.com/observability/runtime-cache

Available Metrics:

  • Cache Reads: Total number of cache read operations
  • Cache Writes: Total number of cache write operations
  • Hit Rate: Percentage of successful cache hits (target: >90%)
  • On-demand Revalidations: Number of manual cache invalidations

The dashboard provides real-time monitoring with configurable time ranges and detailed performance analytics for optimizing cache strategies.

Cache Health Monitoring

Cache health is monitored through:

  • Hit/Miss Ratios: Tracking cache effectiveness
  • Revalidation Frequency: Monitoring content freshness
  • Edge Cache Distribution: Ensuring global cache coverage
  • Performance Impact: Measuring cache contribution to page speed

Cache Invalidation Strategies

Content-Driven Invalidation

Content update cache invalidation
// When content is updated in CMS
const contentTags = [
`node_${nodeId}`,
`paragraph_${paragraphId}`,
`media_${mediaId}`,
`taxonomy_term_${termId}`
];

// Invalidate all related cache entries
contentTags.forEach(tag => {
revalidateTag(tag);
});

Time-Based Invalidation

Time-based cache strategies
// Short-lived dynamic content (5 minutes)
export const revalidate = 300;

// Medium-lived content (30 minutes)
export const revalidate = 1800;

// Long-lived content (24 hours)
export const revalidate = 86400;

User-Driven Invalidation

Manual cache invalidation
// Admin-triggered cache clearing
POST /api/revalidate?type=full&source=education

// Content editor cache refresh
POST /api/revalidate?tags=node_123,paragraph_456&source=education

// Path-specific invalidation
POST /api/revalidate?path=/learn/investing&source=education

Performance Optimizations

Incremental Static Regeneration (ISR)

ISR configuration
export const revalidate = 1800; // Revalidate every 30 minutes
export const dynamicParams = true; // Generate pages on-demand
export const dynamic = 'force-static'; // Static generation preferred

Streaming and Suspense

Streaming with cache optimization
import { Suspense } from 'react';

export default function Page() {
return (
<div>
<Suspense fallback={<Loading />}>
<CachedContent />
</Suspense>
</div>
);
}

Cache Configuration Examples

Development Environment

Development cache configuration
const nextConfig = {
experimental: {
useCache: true,
},
// Disable cache in development for immediate updates
...(process.env.NODE_ENV === 'development' && {
experimental: {
useCache: false,
},
}),
};

Production Environment

Production cache configuration
const nextConfig = {
experimental: {
useCache: true,
},
// Aggressive caching in production
headers: async () => [
{
source: '/((?!api/).*)',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, stale-while-revalidate=86400',
},
],
},
],
};

Cache Debugging and Tools

Cache Inspection

Cache debugging commands
# Check cache status
curl -I "https://www.schwab.com/learn/story/example"

# Force cache refresh
curl -X POST "https://www.schwab.com/api/revalidate?path=/learn/story/example"

# Inspect cache tags
curl -H "Cache-Control: no-cache" "https://www.schwab.com/api/content/example"

Development Tools

Cache debugging utilities
// Log cache status in development
if (process.env.NODE_ENV === 'development') {
console.log('Cache Tags:', cacheTagList);
console.log('Revalidation Time:', revalidate);
console.log('Dynamic Params:', dynamicParams);
}

Best Practices

Cache Key Design

  1. Hierarchical Structure: Use hierarchical cache keys for easy bulk invalidation
  2. Source Prefixing: Include data source in cache keys (education_, drupal_)
  3. Granular Tags: Use specific tags for precise invalidation control

Cache Timing Strategy

Content TypeRevalidation TimeStrategy
Financial Data5 minutes (300s)Short-lived, frequently updated
Educational Content30 minutes (1800s)Medium-lived, periodic updates
Static Marketing24 hours (86400s)Long-lived, infrequent updates
User-GeneratedOn-demandEvent-driven invalidation

Error Handling

Cache error handling
try {
const cachedData = await fetchWithCache(url, { tags: cacheTagList });
return cachedData;
} catch (error) {
// Fallback to uncached request
SchLogger.error(`Cache fetch failed: ${error.message}`);
return await fetchWithoutCache(url);
}

The caching architecture provides a robust, scalable, and performant foundation for the Charles Schwab digital properties, ensuring optimal user experience while minimizing server load and infrastructure costs.