./ahmedhashim

Caching with PromiseMaps

Delivering value to customers in real-time often means performing expensive computations on the fly. A simple data structure called a PromiseMap can help reduce system load while maintaining responsiveness.

The Problem

Imagine a social network where popular user profiles get requested many times per second. A naive cache implementation might look like this:

const cache: Map<number, UserProfile> = new Map();

const getProfile = async (userId: number): Promise<UserProfile> => {
  if (!cache.has(userId)) {
    cache.set(userId, await db.getUserProfile(userId));
  }
  return cache.get(userId);
};

This has a critical flaw: the cache isn’t populated until after the await completes. During this window, every concurrent request triggers a separate database query: a “cache stampede.”

The Solution

Cache the promise, not the value:

const cache: Map<number, Promise<UserProfile>> = new Map();

const getProfile = async (userId: number): Promise<UserProfile> => {
  if (!cache.has(userId)) {
    cache.set(userId, db.getUserProfile(userId));
  }
  return cache.get(userId);
};

Now concurrent requests await the same promise. Only one database query occurs per unique key.

Why It Works

A PromiseMap is just a Map<Key, Promise<Value>>. This pattern works because promises execute immediately when created. Multiple awaits on the same promise return the same value, and once resolved promises cache their result permanently.

This makes refetching the same request cost practically nothing.

Error handling

Failed promises need cleanup to allow for retries:

cache.set(
  userId,
  db.getUserProfile(userId).catch((error) => {
    cache.delete(userId); // Allow retry on failure
    throw error;
  }),
);

PromiseMaps solve cache stampedes by caching promises rather than values. While they require careful error handling and memory management, they can reduce backend load by orders of magnitude for frequently accessed resources.