Caching is a powerful technique to enhance the performance and scalability of applications. By storing frequently accessed data in faster storage layers, you can reduce latency, alleviate database load, and provide a smoother user experience.
In this article, we’ll explore the top five caching patterns that every developer should know. Using TypeScript and Node.js, we’ll demonstrate how these patterns can be implemented to optimize application performance.
1️⃣ In-Memory Cache
An in-memory cache stores data in memory for fast read and write operations. It is ideal for storing small, frequently accessed data like session information or configuration settings.
Example: Caching with Node.js
class InMemoryCache<T> {
private cache: Map<string, T> = new Map();
set(key: string, value: T) {
this.cache.set(key, value);
}
get(key: string): T | undefined {
return this.cache.get(key);
}
delete(key: string) {
this.cache.delete(key);
}
}
// Usage
const cache = new InMemoryCache<number>();
cache.set('user:123', 42);
console.log(cache.get('user:123')); // Output: 42
2️⃣ Write-Through Cache
In a write-through cache, writes are first made to the cache and then immediately written to the underlying data store. This ensures that the cache is always in sync with the database.
Example: Write-Through Cache
class WriteThroughCache<T> {
private cache: Map<string, T> = new Map();
async write(key: string, value: T, writeToDb: (key: string, value: T) => Promise<void>) {
this.cache.set(key, value);
await writeToDb(key, value); // Write to database
}
get(key: string): T | undefined {
return this.cache.get(key);
}
}
// Simulated database function
async function dbWrite(key: string, value: any) {
console.log(`Writing ${key}: ${value} to database`);
}
// Usage
const writeCache = new WriteThroughCache<string>();
writeCache.write('key1', 'value1', dbWrite);
console.log(writeCache.get('key1')); // Output: 'value1'
3️⃣ Cache-aside Pattern
In the cache-aside pattern, the application checks the cache first. If the data is not available, it retrieves it from the database and stores it in the cache for future requests.
Example: Cache-aside with Redis
import redis from 'redis';
import { promisify } from 'util';
const client = redis.createClient();
const getAsync = promisify(client.get).bind(client);
const setAsync = promisify(client.set).bind(client);
async function getData(key: string, fetchFromDb: () => Promise<string>): Promise<string> {
const cachedValue = await getAsync(key);
if (cachedValue) {
console.log('Cache hit');
return cachedValue;
}
console.log('Cache miss');
const value = await fetchFromDb();
await setAsync(key, value);
return value;
}
// Simulated database fetch
async function fetchFromDb() {
return 'Database Value';
}
// Usage
getData('user:123', fetchFromDb).then(console.log);
4️⃣ Read-Through Cache
In a read-through cache, all read requests go through the cache. If the data is not present, the cache fetches it from the data store and updates itself automatically.
Example: Read-Through Cache
class ReadThroughCache<T> {
private cache: Map<string, T> = new Map();
async get(key: string, fetchFromDb: () => Promise<T>): Promise<T> {
if (this.cache.has(key)) {
console.log('Cache hit');
return this.cache.get(key)!;
}
console.log('Cache miss');
const value = await fetchFromDb();
this.cache.set(key, value);
return value;
}
}
// Usage
const readCache = new ReadThroughCache<string>();
readCache.get('user:123', async () => 'Fetched from DB').then(console.log);
5️⃣ Distributed Cache
A distributed cache stores data across multiple nodes, ensuring scalability and fault tolerance. It is commonly used in large-scale systems to handle high traffic efficiently.
Example: Using Redis as a Distributed Cache
import { createClient } from 'redis';
const client = createClient();
async function setCache(key: string, value: string) {
await client.connect();
await client.set(key, value);
console.log(`Set ${key}: ${value}`);
await client.disconnect();
}
async function getCache(key: string) {
await client.connect();
const value = await client.get(key);
console.log(`Get ${key}: ${value}`);
await client.disconnect();
return value;
}
// Usage
setCache('session:abc', 'active');
getCache('session:abc');
Caching patterns are essential for building high-performance, scalable applications. Whether you’re using simple in-memory caches or complex distributed caching solutions, choosing the right pattern for your use case can significantly improve your application’s speed and reliability.
By implementing these patterns in Node.js with TypeScript, you can optimize your system's performance and enhance user experience. Start with the basics and scale as your application's needs evolve.
Happy coding ❤️
Top comments (0)