DEV Community

Wallace Freitas
Wallace Freitas

Posted on

Top 5 Caching Patterns for High-Performance Applications

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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.

List of strategies of cache with diagrams

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)