CACHE-ASIDE, WRITE-AROUND, WRITE-THROUGH

Introduction

  • In this article, we will explore how to implement the simplest and most commonly used patterns, including CACHE-ASIDE (LAZY LOADING), WRITE-AROUND and WRITE-THROUGH.
  • Among these, CACHE-ASIDE and WRITE-AROUND are often used together.
  • I will use NestJS combined with Redis and Postgres, so please set up the necessary components before starting.
  • You can review the theoretical content in my previous article.
alt text

Detail

First, create the schema as follows in drizzle-orm/schema/schema.ts:

import {
  bigserial,
  decimal,
  integer,
  pgTable,
  serial,
  text,
  timestamp,
  varchar,
} from 'drizzle-orm/pg-core'

export const products = pgTable('products', {
  id: bigserial('id', {mode: 'number'}).primaryKey(),
  name: varchar('name', {length: 255}).notNull(),
  description: text('description'),
  price: decimal('price', {precision: 10, scale: 2}).notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
})

export const carts = pgTable('carts', {
  id: serial('id').primaryKey(),
  userId: integer('user_id').notNull().unique(),
  itemsJson: varchar('items_json', {length: 2000}).notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow(),
})

This is ValkeyModule, which will function similarly to Redis, located at module/valkey.module.ts:

import {Module} from '@nestjs/common'
import {ConfigModule, ConfigService} from '@nestjs/config'
import Redis from 'ioredis'

@Module({
  imports: [ConfigModule],
  providers: [
    {
      provide: 'VALKEY_CLIENT',
      useFactory: (configService: ConfigService) =>
        new Redis({
          host: configService.get<string>('VALKEY_HOST'),
          port: configService.get<number>('VALKEY_PORT'),
        }),
      inject: [ConfigService],
    },
  ],
  exports: ['VALKEY_CLIENT'],
})
export class ValkeyModule {}

Create the file service/products.service.ts:

import {Inject, Injectable} from '@nestjs/common'
import {eq} from 'drizzle-orm'
import {NodePgDatabase} from 'drizzle-orm/node-postgres'
import Redis from 'ioredis'
import * as schema from 'src/drizzle-orm/schema/schema'

@Injectable()
export class ProductsService {
  private readonly CACHE_TTL = 3600

  constructor(
    @Inject('DRIZZLE_DB') private readonly db: NodePgDatabase<typeof schema>,
    @Inject('VALKEY_CLIENT') private readonly valkey: Redis
  ) {}

  async getProductById(id: number) {
    const cacheKey = `product:${id}`

    const cachedProduct = await this.valkey.get(cacheKey)
    if (cachedProduct) {
      return JSON.parse(cachedProduct)
    }

    const product = await this.db
      .select()
      .from(schema.products)
      .where(eq(schema.products.id, id))
      .then(res => res[0])

    if (!product) {
      return null
    }

    await this.valkey.set(
      cacheKey,
      JSON.stringify(product),
      'EX',
      this.CACHE_TTL
    )

    return product
  }

  async updateProduct(
    id: number,
    data: Partial<{name: string; price: string; description: string}>
  ) {
    const [updatedProduct] = await this.db
      .update(schema.products)
      .set({...data, updatedAt: new Date()})
      .where(eq(schema.products.id, id))
      .returning()

    await this.valkey.del(`product:${id}`)

    return updatedProduct
  }
}
  • The getProductById function implements CACHE-ASIDE (LAZY LOADING) FOR READ. It reads from the cache first and if not present, queries the Database.
  • The updateProduct function implements WRITE-AROUND. After updating the product in the Database, it invalidates the cache so subsequent queries fetch the latest product data.

Create the file service/cart.service.ts:

import {Inject, Injectable} from '@nestjs/common'
import {eq} from 'drizzle-orm'
import {NodePgDatabase} from 'drizzle-orm/node-postgres'
import Redis from 'ioredis'
import * as schema from 'src/drizzle-orm/schema/schema'

@Injectable()
export class CartService {
  constructor(
    @Inject('DRIZZLE_DB') private readonly db: NodePgDatabase<typeof schema>,
    @Inject('VALKEY_CLIENT') private readonly valkey: Redis
  ) {}

  async updateCart(userId: number, items: any[]) {
    const cacheKey = `cart:${userId}`
    const itemsJson = JSON.stringify(items)

    const existingCart = await this.db
      .select()
      .from(schema.carts)
      .where(eq(schema.carts.userId, userId))
      .then(res => res[0])

    let updatedCart
    if (existingCart) {
      ;[updatedCart] = await this.db
        .update(schema.carts)
        .set({itemsJson, updatedAt: new Date()})
        .where(eq(schema.carts.userId, userId))
        .returning()
    } else {
      ;[updatedCart] = await this.db
        .insert(schema.carts)
        .values({userId, itemsJson})
        .returning()
    }

    await this.valkey.set(cacheKey, JSON.stringify(updatedCart))

    return updatedCart
  }

  async getCart(userId: number) {
    const cacheKey = `cart:${userId}`

    const cachedCart = await this.valkey.get(cacheKey)
    if (cachedCart) {
      return JSON.parse(cachedCart)
    }

    const dbCart = await this.db
      .select()
      .from(schema.carts)
      .where(eq(schema.carts.userId, userId))
      .then(res => res[0])

    if (dbCart) {
      await this.valkey.set(cacheKey, JSON.stringify(dbCart))
    }

    return dbCart
  }
}
  • The updateCart function implements WRITE-THROUGH. After updating the Database, it immediately writes to the cache, keeping the cache 100% synchronized with the Database.
  • The getCart function operates like CACHE-ASIDE, but in most cases, it retrieves data directly from the cache without querying the Database.

Create the file controller/products.controller.ts:

import {
  Body,
  Controller,
  Get,
  NotFoundException,
  Param,
  ParseIntPipe,
  Put,
} from '@nestjs/common'
import {ProductsService} from 'src/service/products.service'

@Controller('products')
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}

  @Get(':id')
  async getProduct(@Param('id', ParseIntPipe) id: number) {
    const product = await this.productsService.getProductById(id)
    if (!product) throw new NotFoundException('Product not found')
    return product
  }

  @Put(':id')
  async updateProduct(
    @Param('id', ParseIntPipe) id: number,
    @Body() body: {name?: string; price?: string; description?: string}
  ) {
    return await this.productsService.updateProduct(id, body)
  }
}

Create the file controller/cart.controller.ts:

import {Body, Controller, Get, Param, ParseIntPipe, Put} from '@nestjs/common'
import {CartService} from 'src/service/cart.service'

@Controller('carts')
export class CartController {
  constructor(private readonly cartService: CartService) {}

  @Get(':userId')
  async getCart(@Param('userId', ParseIntPipe) userId: number) {
    return await this.cartService.getCart(userId)
  }

  @Put(':userId')
  async updateCart(
    @Param('userId', ParseIntPipe) userId: number,
    @Body() body: {items: any[]}
  ) {
    return await this.cartService.updateCart(userId, body.items)
  }
}

Modify the app.module.ts file (drizzleProvider was covered in the previous article):

import {Module} from '@nestjs/common'
import {ConfigModule} from '@nestjs/config'
import {CartController} from './controller/cart.controller'
import {ProductsController} from './controller/products.controller'
import {drizzleProvider} from './drizzle-orm'
import {ValkeyModule} from './module/valkey.module'
import {CartService} from './service/cart.service'
import {ProductsService} from './service/products.service'

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
    ValkeyModule,
  ],
  controllers: [
    ProductsController,
    CartController,
  ],
  providers: [
    drizzleProvider,
    ProductsService,
    CartService,
  ],
})
export class AppModule {}

Finally, the .env file:

VALKEY_HOST = <VALKEY_HOST>
VALKEY_PORT = 6379

DATABASE_URL = postgresql://{username}:{password}@{url}:5432/{database}

The results are as follows:alt textalt textalt textalt text

Happy coding!

See more articles here.

Comments

Popular posts from this blog

All Practice Series

Understanding React Server Component

Kubernetes Deployment for Zero Downtime

Sitemap

Deploying a NodeJS Server on Google Kubernetes Engine

React Practice Series

Helm for beginer - Deploy nginx to Google Kubernetes Engine

Docker Practice Series

A Handy Guide to Using Dynamic Import in JavaScript

DevOps Practice Series