Architecting for Speed and Scale
The Foundation of Performance
Relative to your application's memory, your database is slow. Every query involves:
If 1000 users all request the same popular product page, you are making 1000 identical, slow queries to your database.
A cache is a high-speed data storage layer which stores a subset of data, transient in nature, so that future requests for that data are served up faster than is possible by accessing the data's primary storage location.
Analogy: The librarian's desk. Instead of walking to the back of the library (the database) for a popular book every time, the librarian keeps a copy on their desk (the cache). Subsequent requests are served instantly.
The Simplest First Step
The simplest cache is one that lives inside your application's own memory. We can implement a basic version using a JavaScript `Map`.
// A very simple in-memory cache
const cache = new Map();
function get(key: string) {
return cache.get(key);
}
function set(key: string, value: any, ttl_seconds: number) {
cache.set(key, value);
// After a certain time (Time To Live), delete the key
setTimeout(() => {
cache.delete(key);
}, ttl_seconds * 1000);
}
While simple, this approach has two critical problems for production systems:
In-memory caching is for single-instance applications or very specific use cases only.
The Distributed In-Memory Datastore
Remote Dictionary Server. It is an open-source, in-memory, key-value data store. It runs as a separate server that all of your application instances can connect to.
It solves both problems of the in-memory cache:
Redis is more than just a simple key-value store. Its values can be complex data types.
You interact with Redis using simple commands.
# Set a key with a value
> SET user:1 '{"name": "Alex", "email": "alex@example.com"}'
# Get a value by its key
> GET user:1
# Set a key with an expiration of 60 seconds (Time To Live)
> SET user:2 '{"name": "Jane"}' EX 60
# Delete a key
> DEL user:1
The `@nestjs/cache-manager`
NestJS provides a unified caching module, `@nestjs/cache-manager`, which provides a standard API for caching and can be configured to use different storage engines.
npm install @nestjs/cache-manager cache-manager-redis-store
In your `AppModule`, you import and configure the `CacheModule` to use Redis.
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import * as redisStore from 'cache-manager-redis-store';
@Module({
imports: [
CacheModule.register({
isGlobal: true, // Make the cache available everywhere
store: redisStore,
host: 'localhost',
port: 6379,
ttl: 300, // Default time-to-live in seconds
}),
],
})
export class AppModule {}
You can then inject the cache manager into any service using the `@Inject(CACHE_MANAGER)` decorator.
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
@Injectable()
export class UsersService {
constructor(
@Inject(CACHE_MANAGER) private cacheManager: Cache,
// ... other dependencies
) {}
async findOne(id: number): Promise {
const key = `user_${id}`;
// Check the cache first
const cachedUser = await this.cacheManager.get(key);
if (cachedUser) {
console.log('Serving from cache!');
return cachedUser;
}
// If not in cache, get from DB
console.log('Serving from database...');
const user = await this.usersRepository.findOneBy({ id });
// Store the result in the cache for next time
await this.cacheManager.set(key, user);
return user;
}
}
The Most Common Caching Strategy
The code we just wrote is a perfect example of the "Cache-Aside" pattern. The application code is responsible for managing the cache.
What happens when a user's data is updated? The data in our cache is now stale and incorrect.
We must invalidate the cache. When we update or delete a user, we must also explicitly delete their entry from the cache.
async update(id: number, updateUserDto: UpdateUserDto): Promise {
// ... logic to update the user in the database ...
const updatedUser = await this.usersRepository.save(user);
const key = `user_${id}`;
// Invalidate (delete) the old data from the cache
await this.cacheManager.del(key);
return updatedUser;
}
The next time this user is requested, it will be a cache miss, the app will fetch the fresh data from the database, and repopulate the cache.
You will be given a NestJS project with a `CacheModule` already configured. Your task is to implement the cache-aside pattern for the `findAll()` method in the `UsersService`.