> Jitrak Blog

>_ Redis cache decorator for Hexagonal architecture in NestJS

# Redis cache decorator for Hexagonal architecture in NestJS

@ Yosapol Jitrak | 31 Jan 2025 05:30

ล่าสุด ผมได้มีโอกาสไปพูดในงาน JavaScript Bangkok 2.0.0 ในหัวข้อ Redis cache decorator for Hexagonal architecture in NestJS แต่ในงานมีเวลาจำกัด และจอมีปัญหา เลยคิดว่าจะมาเขียนเล่าใน Blog อีกครั้ง เพื่อให้คนที่สนใจสามารถอ่านเพิ่มเติมได้

ปัญหา

เราจะมาเล่าถึงปัญหากันก่อน ปัจจุบันงานที่ทำผมทำอยู๋ใช้ MongoDB Atlas และทาง Business อยากจะลด Cost ครับ ซึ่งราคาจะเป็นตามรูปด้านล่างนี้ครับ

MongoDB Atlas Pricing

แน่นอนปัญหานี้เราน่าจะใช้ Cache มาช่วย เพื่อลด Hit ของ Database ครับ ปัจจุบันก็คงจะเลือกใช้เป็น Redis ครับ

Redis Logo

Project ที่ผมทำอยู่เป็น NestJS และเป็น Hexagonal Architecture ครับ ซึ่งการทำ Cache สามารถใช้ Cache Manager ของ NestJS ได้เลย แต่ผมก็ติดปัญหาอยู่ตรงเมื่อเราต้องการทำ Invalidate cache ทันทีเมื่อมีการ Update ข้อมูลใหม่ ๆ ใน Database ครับ ซึ่ง Cache Manager ของ NestJS ก็ยังไม่มีตรงส่วนนี้ เราสามารถทำได้เพียงแค่ใช้ Time To Live (TTL) เท่านั้น ซึ่งหมายความว่ายังไม่ตอบโจทย์การใช้งานของผม

NestJS Cache Manager Credit: NestJS Cache

Decorator of TypeScript

ก่อนจะไปถึงวิธีแก้ปัญหา ผมขอเล่าถึง Decorator ใน TypeScript ก่อน เพราะจะเป็นสิ่งที่ช่วยเราในการแก้ไขปัญหา ใน TypeScript เรามีสิ่งที่เรียกว่า Decorator อยู่ ถ้าเปรียบเทียบให้เห็นภาพ ภาษาอื่น และ Framework อื่น ๆ จะมีสิ่งที่คล้าย ๆ กันอยู่ครับ เรามาลองดูกันว่ามีตัวไหนบ้าง

  1. Annotation in Java ยกตัวอย่างที่หลาย ๆ คนน่าจะเคยเจอกันมาในภาษา Java และใช้ Framework Spring Boot น่าจะคุ้นเคยกันดีกับ Annotation ครับ

Annotations Spring Boot Credit: Spring Annotations Cheat Sheet

  1. Attribute in C# หลายคนที่เคยเขียน C# แล้วใช้ NUnit เขียน Test ก็น่าจะเคยเห็น Attribute ผ่านหน้าผ่านตากันมาบ้างครับ

NUnit Attribute Credit: NUnit Attribute

คราวนี้เราลองมาดูตัวอย่าง Decorator ของ NestJS กันครับ

NestJS Decorator 1 NestJS Decorator 2 NestJS Decorator 3 NestJS Decorator 4

Hexagonal Architecture

  • คิดโดย Alistair Cockburn ในปี 2005
  • อีกชื่อคือ “Ports and Adapters Architecture.”

ในบทความนี้คงไม่ได้เล่าลงลึกมากครับ หลัก ๆ แล้วตัว Hexagonal Architecture นั้นจะแบ่ง Layer เป็นชั้น ๆ โดยจะทำให้ส่วนที่ผูกติดกับ Framework รวมถึงภายนอกอย่าง Database หรือ API นั้นแยกออกจากส่วนที่เป็น Business Logic ครับ ซึ่งจะเพิ่มความยืดหยุ่นกับ Code ของเราสามารถเปลี่ยนแปลงไปใช้ Framework และ Database อื่นได้ง่ายขึ้น รวมถึงสามารถทำ Unit Test ของ Business Logic ได้ง่ายขึ้นมากครับ โดยภายนอกและภายใน Application จะคุยกันผ่าน Port ครับ โดย Port นั้นจะเป็นเพียงแค่ Interface หลังจากนั้นเราค่อยเอาตัวที่จะเชื่อมต่อมา Implement จริงเป็น Concrete class อีกทีนึงครับ

Hexagonal Architecture Credit: Hexagonal Architecture, there are always two sides to every story

Explicit Architecture Credit: DDD, Hexagonal, Onion, Clean, CQRS, … How I put it all together

ยกตัวอย่างตามรูปด้านล่างนี้ครับ

Hexagonal flow

ลองมาดู Code ตัวอย่างกันครับ

example-hexagonal-code-01

  • Controller เรียก Use Case

example-hexagonal-code-02

  • Use Case เรียก Repository

example-hexagonal-code-03

  • Repository เป็นแค่ Interface ที่ดึงข้อมูล

example-hexagonal-code-04

  • Real implementation ของ Repository จะทำการดึงข้อมูลจาก Database พร้อมกับแปลงข้อมูลเป็น Domain ที่ใช้งานใน Application ของเราครับ จะเห็นว่าข้อมูลที่เราส่งกลับไปให้ Application นั้นจะไม่ได้เป็นข้อมูลที่มาจาก Database โดยตรง และไม่มี Type ของ Database หลุดกลับออกไปใน Application ครับ

แก้ปัญหา

หลังจากปูพื้นกันมาแล้ว เรากลับมาเข้าเรื่องปัญหาของเราดีกว่าครับ เราจะแก้ปัญหากันอย่างไรได้บ้าง

First Solution

  • เลือกเองใน use-case ว่าจะใช้ Cache หรือไม่
  • แต่ Code ใน use-case จะมีส่วนที่ไม่เกี่ยวข้องกับ Business Logic อยู่
  • ต้องไปไล่แก้ Test cases ทั้งหมด
  • Code ที่เปลี่ยนมหาศาล

First solution cod

  • จะเห็นว่า Code ที่ใส่กรอบสีแดงไว้จะต้องไปไล่เพิ่มทุก Use Case ที่มีการใช้ Cache ครับ
  • ดูแล้วยังไม่ตอบโจทย์เรา

Second Solution

  • เลือกเองใน Repository ว่าจะใช้ Cache หรือไม่
  • ไม่ต้องไปไล่แก้ Test cases
  • Code ที่เปลี่ยนมหาศาล
  • มีโอกาส Human Error เกิดขึ้น เพราะไม่มี Test ตรงส่วนนี้

Second solution code

  • จะเห็นว่า Code ที่ใส่กรอบสีแดงไว้จะต้องไปไล่เพิ่มทุก Repository ที่มีการใช้ Cache ครับ
  • ดูแล้วยังไม่ตอบโจทย์เราอีกแล้ว

oh-my-head

think-meme คราวนี้เราลองมาใช้ความคิดต่ออีกหน่อย เห้ยจริง ๆ เราใช้ Decorator ของ NestJS เต็มบ้านเต็มเมืองใน Code ของเราอยู่แล้วนี่นา คิดออกมาได้เป็นไอเดียสุดท้ายครับ

Firnal Solution

  • เอา Decorator แปะบนหัวของ methods in Mongo repository (Adapter)
  • ไม่ต้องไปไล่แก้ Test cases ที่มีอยู่
  • สามารถทำ Unit test สำหรับ Decorator ได้
  • มี Code นิดหน่อยที่ต้องทำ
  • ไม่มี Human Error กับ Test cases ใน Mongo repository

Decorator

กลับมาดูกันว่า Decorator ของ Method เราทำยังไงกับมันได้ครับ

Decorator 01 จะเห็นว่าเราสามารทำอะไรก่อน และหลัง Method ของเราได้ครับ

ซึ่งมีคนคิดคล้ายเรา ๆ เลย ลองดูตัวอย่างด้านล่างนี้ครับ

Memoize Decorator จะเห็นว่าเราเก็บ Original method ไว้ และสุดท้ายคืน descriptor ตัวเดิมกลับออกไป

คราวนี้ลองดู Implement จริงครับ Memoize Decorator Credit: How To Create a Custom Typescript Decorator

คราวนี้ลองมาดูไอเดียของเรากันครับ

Redis Cache for Repository

Redis Cache for Repository

Invalidate Redis Cache for Repository

Invalidate Redis Cache for Repository

หลายคนมาถึงตรงนี้แล้วอาจจะงงว่า Combination key คืออะไร ลองดูตัวอย่างด้านล่างนี้ครับ

Demo combination keys

สรุปก็คือความเป็นไปได้ของ Cache key ของเราทั้งหมดกับ Repository นั้นครับ

Demo code

Demo cache for repository with items mongo repository 1

จะเห็นว่า Code เราเวลาจะต้องการจะทำ Cache ก็แค่แปะ CacheForRepository decorator เข้าไปใน Method ที่เราต้องการจะทำ Cache ได้เลยครับ โดยตัวอย่างนี้ผมให้ Focus แค่ส่วน Cache ก่อนนะครับ โดยอันนี้ Key ที่จะทำการ Cache คือ items:all นั้นเอง

คราวนี้ลองมาดูตัวอย่าง Cache อื่น ๆ กันบ้างครับ

Demo cache for repository with items mongo repository 2

ตัวอย่างนี้จะเป็น Key items:{id:?} เราสามารถแทนค่า ? ได้จาก itemId ที่ส่งเข้ามาใน Method นี้ครับ

ลองไปดูตัวอย่างถัดไปกันครับ

Demo cache for repository with items mongo repository 3

  • Key items:{status:?,color:?} เราสามารถแทนค่า ? ได้จาก status และ color ที่ส่งเข้ามาใน Method findByStatusAndColor ครับ
  • Key items:{country:?,category:?} เราสามารถแทนค่า ? ได้จาก country และ category ที่ส่งเข้ามาใน Method findByCountryAndCategory ครับ

ต่อมาเรามาดูส่วนของ Invalidate Cache กันครับ

Invalidate cache for repository with items mongo repository 1

  • เวลาเรา Invalidate Cache เราจะต้องกำหนดค่า keyCombinations ที่จะทำการ Invalidate ครับ ซึ่งจะมีรูปแบบคือ [['id'], ['country', 'category'], ['status', 'color']] ซึ่งจะทำการ Invalidate ทุก Key ที่มีความเป็นไปได้ทั้งหมดที่อยู่ใน Array นี้ครับ
  • สำหรับ Method update จะไปทำการ Invalid cache ตัว Key ที่มีหน้าตาตามตัวอย่างจะประมาณนี้ครับ items:all, items:{id:507f1f77bcf86cd799439011}, items:{country:Thailand,category:Clothes} และ items:{status:Available,color:Red} ตามข้อมูลที่ Update เข้ามาครับ
  • แต่ id สำหรับ Method create จะไม่มีผล เพราะยังไม่เคยมี id นี้มาก่อน แต่เราก็ใส่ไปเพื่อกันความงงที่จะใส่บ้างไม่ใส่บ้างครับ

คราวนี้ลองมาดูตัว Decorator ที่เราเขียนกันครับ

ตัว Cache Decorator จะมีหน้าตาตาม Code ด้านล่างนี้ครับ Cache Decorator

ตัว Invalidate Decorator จะมีหน้าตาตาม Code ด้านล่างนี้ครับ Invalidate Cache Decorator

สำหรับตัวอย่างนี้ผมไม่ได้เล่าทั้งหมดครับ แต่จะเป็น Point ที่สำคัญครับ จะมีตัวอย่างทั้งหมดใน Github ครับ
GitHub: redis-cache-decorator-for-hexagonal-architecture-in-nestjs

Slide: Redis cache decorator for hexagonal architecture in NestJS

จากทั้งหมดนี้จะเห็นว่าเราใช้เวลาคิดหน่อย แต่จะช่วยลดงาน และความซับซ้อนของ Code ไปได้เยอะเลยครับ

ที่ปล่อยบทความช้า เพราะอยากจะให้ทาง JavaScript Bangkok 2.0.0 ปล่อยตัว Video ที่ถ่ายไว้ในงานสู่สาธารณะก่อนครับ

═══════════════════ EOF ═══════════════════