You met in-process caching in the previous lesson and it works beautifully - as long as you have one server. Start a second instance behind a load balancer and each builds its own cache. A user lands on one, then the other, and gets different data. Worse: invalidating an entry on the first instance does not remove it from the second.
A legion did not keep supplies in every soldier's pack. It had a shared storehouse everyone drew from. Redis is such a storehouse - and that is its main advantage over an in-memory cache: sharing the cache between many application instances.
It is not that it is always faster - an in-process cache is often faster, because it needs no network round trip. It is that every instance sees the same thing.
Redis in NestJS serves four purposes at once: cache, sessions, pub/sub and queue.
Cache is where people usually start. Sessions - the user sessions you met with authentication; thanks to a shared store, logging in on one instance holds on all of them. Pub/sub - broadcasting events between services. Queue - the job queues we returned to with the Empire's couriers.
That is why Redis appears in almost every stack: one service covers four needs that would otherwise take four separate tools.
Wiring Redis up as the cache store always goes the same way:
cache-manager-redis-store.CacheModule with the Redis store.CACHE_MANAGER in the service.cache.get() and cache.set() with keys.Step two looks like this:
1@Module({
2 imports: [
3 CacheModule.registerAsync({
4 isGlobal: true,
5 useFactory: (config: ConfigService) => ({
6 store: redisStore,
7 host: config.get('REDIS_HOST'),
8 port: config.get('REDIS_PORT'),
9 ttl: 300,
10 }),
11 inject: [ConfigService],
12 }),
13 ],
14})
15export class AppModule {}Note one field:
. It is what turns the default in-memory cache into Redis - the rest of the configuration merely says where that Redis lives. The whole application uses the same interface; swapping the store touches not one line in the services.store
ttl: 300 is the default lifetime of an entry in seconds - five minutes. After that Redis removes it by itself.Steps three and four are ordinary code:
1@Injectable()
2export class LegionService {
3 constructor(
4 @Inject(CACHE_MANAGER) private cache: Cache,
5 private repo: LegionRepository,
6 ) {}
7
8 async findOne(id: number) {
9 const key = `legion:${id}`;
10
11 const cached = await this.cache.get<Legion>(key);
12 if (cached) {
13 return cached;
14 }
15
16 const legion = await this.repo.findOne(id);
17 await this.cache.set(key, legion, 600);
18
19 return legion;
20 }
21}@Inject(CACHE_MANAGER) is injection by token - the same mechanism you met with providers that have no class type.The pattern itself is called cache-aside: ask the cache first, return immediately on a hit, and on a miss go to the database and only then store the result. The third argument to
set overrides the default TTL for that single entry.It is worth building keys with a prefix, like
legion:42. Without one, sooner or later two different entities will start using the same number as a key, and the bug will surface at the worst possible moment, @name.Cache-aside writes to the cache only after a read from the database. There is an opposite approach, write-through, in which the cache is written on every data write rather than on a read. The order here is fixed and worth knowing:
The key word is synchronously in step three: the application waits until the data reaches both the cache and the database. Thanks to that the cache is never stale - but a write takes longer than a write to the database alone.
Choosing between the strategies comes down to what you need more. Cache-aside is cheaper on writes and suffices when data changes rarely or brief staleness does no harm. Write-through costs on every write but guarantees consistency - and it earns its keep where a stale read means a real problem, as with a balance or stock level.
The shared storehouse stands and every instance draws from the same one:
cache-manager-redis-store → configure CacheModule with the Redis store → inject CACHE_MANAGER → use cache.get() and cache.set(),store field turns the default in-memory cache into Redis; the services do not change at all,ttl is an entry's lifetime in seconds, and set's third argument overrides it for one key,legion:42) so two entities never start using the same number,In the next lesson we will turn to optimising the application itself - what speeds it up before you even think about a cache. For now remember: an in-memory cache belongs to a process, Redis belongs to everyone - and only that makes the second instance see what the first one sees.