We use cookies to enhance your experience on the site
CodeWorlds

Redis Integration - fast storehouses for tributes

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.

Four uses

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.

Four configuration steps

Wiring Redis up as the cache store always goes the same way:

  1. Install the packages:
    cache-manager-redis-store
    .
  2. Configure
    CacheModule
    with the Redis store.
  3. Inject
    CACHE_MANAGER
    in the service.
  4. Use
    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:

store
. 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.

ttl: 300
is the default lifetime of an entry in seconds - five minutes. After that Redis removes it by itself.

Use in a service

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.

Write-through - the other strategy

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:

  1. The application sends a write.
  2. The cache stores the data.
  3. The cache synchronously writes to the database.
  4. A confirmation is returned to the application.

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.

Summary

The shared storehouse stands and every instance draws from the same one:

  • Redis's main advantage over an in-memory cache is sharing the cache between many application instances - not that it is always faster,
  • Redis in NestJS serves four purposes: cache, sessions, pub/sub and queue,
  • configuration in four steps: install
    cache-manager-redis-store
    → configure
    CacheModule
    with the Redis store → inject
    CACHE_MANAGER
    → use
    cache.get()
    and
    cache.set()
    ,
  • the
    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,
  • cache-aside: ask the cache, on a miss go to the database and only then store the result,
  • build keys with a prefix (
    legion:42
    ) so two entities never start using the same number,
  • write-through in order: the application sends a write → the cache stores the data → the cache synchronously writes to the database → a confirmation returns to the application,
  • cache-aside is cheaper on writes, write-through guarantees consistency - choose by whether a stale read can do harm.

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.

Go to CodeWorlds