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-AROUNDandWRITE-THROUGH. - Among these,
CACHE-ASIDEandWRITE-AROUNDare often used together. - I will use
NestJScombined withRedisandPostgres, so please set up the necessary components before starting. - You can review the theoretical content in my previous article.
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
getProductByIdfunction implementsCACHE-ASIDE (LAZY LOADING)FOR READ. It reads from the cache first and if not present, queries the Database. - The
updateProductfunction implementsWRITE-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
updateCartfunction implementsWRITE-THROUGH. After updating the Database, it immediately writes to the cache, keeping the cache 100% synchronized with the Database. - The
getCartfunction operates likeCACHE-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}
Happy coding!
Comments
Post a Comment