logo
Microservice Design Patterns which you'll face (Now or Future)

Microservice Design Patterns which you'll face (Now or Future)

Published on

Table of Contents

Overview

เวลาเราออกแบบระบบที่ใช้ Microservices architecture สิ่งที่ผมสังเกตเห็นคือ ปัญหาหลาย ๆ อย่างมันวนกลับมาซ้ำ ไม่ว่าจะเป็นเรื่อง data consistency ระหว่าง service, การจัดการ distributed transaction, หรือแม้แต่เรื่องที่ดูง่ายอย่างการ config ค่าต่าง ๆ ให้ service ที่ scale ออกไป 100 instances

บทความนี้ผมรวบรวม design patterns ที่ใช้กันบ่อยใน Microservices architecture โดยแต่ละ pattern จะมีตัวอย่างจากระบบที่เราน่าจะคุ้นเคยกัน เพื่อให้เห็นภาพชัดขึ้นว่ามันแก้ปัญหาอะไร และใช้ตอนไหน

บทความนี้ไม่ได้ครอบคลุมเรื่อง communication patterns อย่าง synchronous (REST, gRPC) และ asynchronous (message queue, event streaming) เพราะเป็นหัวข้อที่ใหญ่พอจะแยกเป็นบทความต่างหากได้


1. Database per Microservice

หลักการคือ แต่ละ service มี database ของตัวเอง ไม่ share database กับ service อื่น ข้อมูลของ service ไหนก็เข้าถึงได้ผ่าน API ของ service นั้นเท่านั้น

ลองนึกถึงระบบ streaming platform ที่มี User Service, Content Service, และ Recommendation Service

flowchart TB
  Client[Client App]
  Client --> US[User Service]
  Client --> CS[Content Service]
  Client --> RS[Recommendation Service]
  US --> DB1[(User DB
PostgreSQL)]
  CS --> DB2[(Content DB
MongoDB)]
  RS --> DB3[(Recommendation DB
Redis)]

User Service ใช้ PostgreSQL เพราะต้องการ relational structure สำหรับ subscription plans และ billing, Content Service เก็บ metadata ของหนัง/ซีรีส์เป็น JSON objects ใน MongoDB เพราะ schema ยืดหยุ่น, ส่วน Recommendation Service ใช้ Redis เพราะต้อง read/write ที่เร็วมากสำหรับ personalized feed

แนวคิดนี้เรียกว่า Polyglot Persistence คือเลือก database ให้เหมาะกับ workload ของแต่ละ service

ข้อดีที่เห็นได้ชัด:

  • แต่ละ service scale database ได้อิสระ
  • schema change ใน Content DB ไม่กระทบ User Service
  • ถ้า Recommendation DB ล่ม user ยังดูหนังได้ (แค่ไม่เห็น personalized feed ชั่วคราว)

2. CQRS (Command Query Responsibility Segregation)

ในระบบ Monolith ทั่วไป read กับ write ใช้ database เดียวกัน พอ application ซับซ้อนขึ้น query ที่ต้อง join 10 กว่า table ก็เริ่มทำให้ database ช้า ขณะเดียวกัน write operation ที่ต้อง validate business logic หลายขั้นตอนก็ไปล็อค table ทำให้ read ช้าตามไปด้วย

CQRS แก้ปัญหานี้โดยแยก read กับ write ออกจากกัน

flowchart LR
  Client[Client]
  Client -->|Write| CMD[Command Handler]
  Client -->|Read| QRY[Query Handler]
  CMD --> WDB[(Write DB
PostgreSQL)]
  WDB -->|Event Sync| RDB[(Read DB
Elasticsearch)]
  QRY --> RDB

ตัวอย่าง: ระบบ search ของ job board — เวลา recruiter โพสต์ตำแหน่งงาน (write) ข้อมูลจะถูกเขียนลง PostgreSQL ก่อน จากนั้น sync ไปยัง Elasticsearch ผ่าน event เวลา candidate ค้นหาตำแหน่งงาน (read) ก็ query จาก Elasticsearch ที่ optimize สำหรับ full-text search โดยเฉพาะ

สิ่งที่ต้องยอมรับกับ pattern นี้คือ eventual consistency — ข้อมูลใน read database จะไม่ up-to-date ทันที ต้องรอ sync ซึ่งอาจใช้เวลาไม่กี่วินาทีถึงไม่กี่นาที ขึ้นอยู่กับ workload


3. Event Sourcing

ปกติเราเก็บแค่ state ล่าสุดของ entity ลงใน database เช่น ยอดเงินในบัญชีตอนนี้ 5,000 บาท แต่ Event Sourcing เก็บทุก event ที่เกิดขึ้นตามลำดับเวลา

flowchart LR
  subgraph Event Store
      E1["1. Account Created
(balance: 0)"]
      E2["2. Deposited
(+10,000)"]
      E3["3. Withdrawn
(-3,000)"]
      E4["4. Transferred
(-2,000)"]
  end
  E1 --> E2 --> E3 --> E4
  E4 -->|Materialized View| CV["Current Balance
5,000"]

ตัวอย่าง: ระบบ Shopping Cart — แทนที่จะเก็บแค่ item list ปัจจุบันของ cart เราเก็บทุก action ที่ user ทำ:

  1. added item A
  2. changed quantity of item A to 3
  3. removed item B
  4. applied coupon X

ทั้งหมดนี้เรียงตามเวลา

ข้อดีคือ:

  • ถ้ามีปัญหา สามารถ replay events เพื่อสร้าง state ใหม่ได้
  • ใช้ audit trail ได้ เพราะมี history ทุก action ครบ
  • debug ง่ายขึ้นเพราะเห็นว่า state เปลี่ยนเพราะ event ไหน

Event Sourcing มักใช้คู่กับ CQRS เพราะ event store ทำหน้าที่เป็น write side ส่วน materialized view ทำหน้าที่เป็น read side


4. Saga Pattern

ใน Monolith เราใช้ database transaction ครอบหลาย operation ได้ง่าย ๆ แต่ใน Microservices แต่ละ service มี database ของตัวเอง จะทำ distributed transaction แบบ 2PC (Two-Phase Commit) ก็ซับซ้อนและ performance ไม่ดี

Saga แก้ปัญหานี้โดยแบ่ง transaction ออกเป็นชุดของ local transactions ถ้าขั้นตอนไหน fail ก็จะ trigger compensating transaction เพื่อ rollback สิ่งที่ทำไปแล้ว

มี 2 แบบ:

Choreography-based Saga

แต่ละ service publish event เอง ไม่มีตัวกลางควบคุม

sequenceDiagram
  participant OS as Order Service
  participant MB as Message Broker
  participant CS as Customer Service

  OS->>MB: Order Created Event
  MB->>CS: (subscribe)
  CS->>CS: Check & Reserve Credit
  alt Credit OK
      CS->>MB: Credit Reserved Event
      MB->>OS: (subscribe)
      OS->>OS: Update Order to Confirmed
  else Credit Not Enough
      CS->>MB: Credit Limit Exceeded Event
      MB->>OS: (subscribe)
      OS->>OS: Update Order to Cancelled
  end

ตัวอย่างที่เห็นในระบบที่ใช้ Kafka เป็น message broker:

E-Commerce Order Flow — เมื่อ user สั่งซื้อสินค้า Order Service publish OrderCreated event ไปยัง Kafka topic จากนั้น Payment Service consume event นี้แล้วตัดเงิน เมื่อสำเร็จก็ emit PaymentCompleted ต่อไปยัง Inventory Service เพื่อ reserve stock แล้ว emit InventoryReserved ให้ Shipping Service จัดส่ง ถ้า payment fail ก็ emit PaymentFailed กลับมาให้ Order Service cancel order และ Inventory Service ปล่อย stock ที่จองไว้ ไม่มี central coordinator แต่ละ service react ต่อ event และ publish event ของตัวเอง

Banking Fund Transfer — Transfer Service emit TransferInitiated ไปยัง Kafka จากนั้น Debit Service ตัดเงินจากบัญชีต้นทางแล้ว emit AccountDebited ต่อให้ Credit Service เติมเงินเข้าบัญชีปลายทางแล้ว emit AccountCredited สุดท้าย Notification Service ส่งแจ้งเตือน ถ้า credit fail ก็ emit CreditFailed กลับมาให้ Debit Service ทำ compensating reversal

ข้อดี: ง่าย เหมาะกับระบบที่มี service ไม่มาก

ข้อเสีย: พอ service เยอะขึ้น flow จะ track ยากเพราะแต่ละ service ต้องรู้ว่าต้อง listen event อะไรบ้าง

Orchestration-based Saga

มี orchestrator คอยสั่งว่า service ไหนต้องทำอะไร ตอนไหน

sequenceDiagram
  participant OC as Order Orchestrator
  participant OS as Order Service
  participant PS as Payment Service
  participant RS as Restaurant Service

  OC->>OS: Create Order (pending)
  OC->>PS: Reserve Payment
  PS-->>OC: Payment Reserved
  OC->>RS: Confirm with Restaurant
  RS-->>OC: Restaurant Confirmed
  OC->>OS: Update Order to Confirmed
  Note over OC: If any step fails,<br/>orchestrator triggers<br/>compensating transactions

ตัวอย่างที่เห็นในระบบที่ใช้ workflow engine อย่าง Temporal หรือ Cadence:

Uber Trip Lifecycle (Cadence) — Uber สร้าง Cadence ขึ้นมาเพื่อ orchestrate workflow ที่ซับซ้อน ตัวอย่างคือ trip saga: orchestrator workflow ควบคุมลำดับตั้งแต่จอง driver, authorize payment, track การเดินทาง, จบ trip, charge rider, จ่ายเงินให้ driver ถ้าขั้นตอนไหน fail (เช่น payment authorization ไม่ผ่าน) orchestrator จะ trigger compensating actions เช่น cancel driver assignment และปล่อย hold on funds โดย Cadence จัดการ retries, timeouts, และ state persistence ให้อัตโนมัติ

Doordash Order Fulfillment (Cadence/Temporal) — Doordash ใช้ Cadence (และประเมิน Temporal) เพื่อ orchestrate order workflow ตั้งแต่ validate order, charge customer, แจ้ง restaurant, assign Dasher (driver), track pickup และ delivery, จนถึง payout ถ้า restaurant cancel orchestrator จะรัน compensations ทั้ง refund customer, ปล่อย Dasher, และอัปเดต order status workflow engine จะ maintain durable state ทำให้ saga survive ได้แม้ process crash โดยไม่สูญเสีย progress

ข้อดี: flow ชัดเจน ดูแลง่าย เหมาะกับระบบที่มีหลาย service เกี่ยวข้อง

ข้อเสีย: orchestrator กลายเป็น single point ที่ต้องดูแลเป็นพิเศษ


5. BFF (Backend for Frontend)

เวลา frontend ต้องแสดงข้อมูลจากหลาย service ในหน้าเดียว เช่น หน้า Dashboard ที่ต้องดึงข้อมูลจาก User Service, Order Service, Notification Service ถ้าให้ frontend เรียก API ทีละตัวแล้วมา aggregate เอง จะเปลืองทั้ง bandwidth และ processing power ของ client

BFF เป็น intermediate layer ที่ aggregate ข้อมูลจากหลาย service แล้วส่ง response ที่พร้อมใช้ให้ frontend

flowchart LR
  Web[Web App] --> WBFF[Web BFF]
  Mobile[Mobile App] --> MBFF[Mobile BFF]
  WBFF --> US[User Service]
  WBFF --> OS[Order Service]
  WBFF --> NS[Notification Service]
  MBFF --> US
  MBFF --> OS

ทำไมต้องแยก BFF ตาม frontend? เพราะ Web กับ Mobile ต้องการข้อมูลไม่เหมือนกัน Web อาจแสดง order history 50 รายการพร้อม detail เต็ม ขณะที่ Mobile แสดงแค่ 10 รายการล่าสุดพร้อม summary สั้น ๆ

BFF ไม่จำเป็นเสมอไป ถ้าแต่ละ service มี UI ของตัวเองหรือ frontend เรียกแค่ 1-2 service ก็ไม่ต้องเพิ่ม layer นี้เข้ามา


6. API Gateway

API Gateway ทำหน้าที่เป็น single entry point สำหรับ client ทุกตัว โดย route request ไปยัง service ที่ถูกต้อง พร้อมจัดการ cross-cutting concerns เช่น authentication, rate limiting, SSL termination, caching

flowchart TB
  C1[Web Client]
  C2[Mobile Client]
  C3[3rd Party]
  C1 --> GW[API Gateway]
  C2 --> GW
  C3 --> GW
  GW --> S1[User Service]
  GW --> S2[Booking Service]
  GW --> S3[Payment Service]
  GW --> S4[Notification Service]

ตัวอย่าง: ระบบ healthcare booking — client ทุกตัว (web, mobile, 3rd party) เรียกผ่าน API Gateway ตัวเดียว ซึ่งจัดการ authn/authz ให้ก่อนจะ forward request ไปยัง service ปลายทาง

สิ่งที่ต้องระวัง:

  • ถ้า API Gateway มีแค่ node เดียว มันจะกลายเป็น single point of failure
  • ถ้ายัด logic เยอะเกินเข้าไปใน gateway มันจะกลายเป็น anti-pattern (fat gateway)
  • ระบบที่ใหญ่ขึ้นควรพิจารณา multiple API Gateways แยกตาม domain

7. Strangler Pattern

ถ้าคุณมี Monolith ที่ใหญ่มากและอยากย้ายไป Microservices การ rewrite ทั้งหมดพร้อมกันเป็นเรื่องเสี่ยง Strangler Pattern ให้เราค่อย ๆ แทนที่ทีละส่วน

flowchart LR
  subgraph P1[Phase 1]
      direction TB
      P1GW[API Gateway] --> P1M[Monolith
100% traffic]
  end

  subgraph P2[Phase 2]
      direction TB
      P2GW[API Gateway]
      P2GW -->|user module| P2MS[User Service]
      P2GW -->|other| P2M[Monolith]
  end

  subgraph P3[Phase 3]
      direction TB
      P3GW[API Gateway]
      P3GW --> P3S1[User Service]
      P3GW --> P3S2[Order Service]
      P3GW --> P3S3[Product Service]
  end

  P1 -->|migrate user module| P2
  P2 -->|migrate remaining| P3

ตัวอย่าง: สมมติมี Monolith ที่รวม User Management, Order Processing, Product Catalog ไว้ด้วยกัน เราเริ่มจากแยก User Management ออกมาเป็น Microservice ก่อน ใช้ API Gateway route traffic ของ user-related requests ไปที่ service ใหม่ ส่วนที่เหลือยังไปที่ Monolith เดิม

ค่อย ๆ ทำแบบนี้ไปเรื่อย ๆ จนกว่า Monolith จะหมด traffic แล้วปิดตัวลง

ขั้นตอนหลัก ๆ:

  1. เลือก module ที่จะแยกออกมาโดยยึดตาม business domain
  2. สร้าง Microservice ใหม่สำหรับ module นั้น
  3. ใช้ API Gateway route traffic บางส่วนไปที่ service ใหม่
  4. ค่อย ๆ เพิ่ม traffic จนครบ 100%
  5. monitor performance แล้วค่อยย้าย module ถัดไป

8. Resilience Patterns

ในระบบ distributed failure เป็นเรื่องปกติ service ล่ม, network timeout, downstream ตอบช้า สิ่งที่ต่างกันระหว่างระบบที่ดีกับระบบที่พังลามคือ วิธีรับมือกับ failure

Resilience patterns ที่ใช้กันบ่อยมีหลายตัว:

  • Circuit Breaker
  • Rate Limit
  • Retry
  • Timeout
  • Bulkhead

8.1 Circuit Breaker

เมื่อ service A เรียก service B แล้ว B ล่ม ถ้าไม่มี Circuit Breaker request จาก A จะ timeout รอเรื่อย ๆ จนกิน thread pool หมด ส่งผลให้ A ล่มตามไปด้วย (cascading failure)

Circuit Breaker ทำงานคล้าย circuit breaker ในระบบไฟฟ้า — ถ้าตรวจพบ failure ถึง threshold ก็ตัดวงจร หยุดส่ง request ไปยัง service ที่มีปัญหา

stateDiagram-v2
  [*] --> Closed
  Closed --> Open: Failure threshold reached
  Open --> HalfOpen: Timeout period expires
  HalfOpen --> Closed: Test request succeeds
  HalfOpen --> Open: Test request fails
  • Closed: ทำงานปกติ ส่ง request ผ่านไปยัง downstream service
  • Open: ตัดวงจร ไม่ส่ง request ไป ตอบ fallback response กลับทันที (เช่น cached data หรือ default value)
  • Half-Open: ลอง request ไปดูว่า downstream กลับมาปกติหรือยัง ถ้าสำเร็จก็กลับไป Closed ถ้ายัง fail ก็กลับไป Open

ตัวอย่าง: Payment Service เรียก External Payment Gateway — ถ้า gateway ตอบช้าหรือ error ติดต่อกัน 5 ครั้งใน 30 วินาที Circuit Breaker จะ trip เป็น Open state แล้วตอบ user ว่า “ระบบชำระเงินขัดข้อง กรุณาลองใหม่ในอีกสักครู่” แทนที่จะปล่อยให้ request ค้างจน timeout

8.2 Retry

บางครั้ง failure เป็นแค่เรื่องชั่วคราว เช่น network blip หรือ downstream service กำลัง restart อยู่ กรณีแบบนี้ ลอง request ใหม่อีกครั้งก็พอ

แต่ Retry ที่ดีต้องมี strategy ไม่ใช่ retry รัว ๆ ทันทีเพราะจะทำให้ downstream ที่กำลังจะฟื้นถูก overload หนักขึ้นไปอีก

sequenceDiagram
  participant A as Booking Service
  participant B as Notification Service

  A->>B: Send Confirmation
  B--xA: 503 Service Unavailable
  Note over A: Wait 500ms (attempt 1)
  A->>B: Send Confirmation (retry 1)
  B--xA: 503 Service Unavailable
  Note over A: Wait 1s (attempt 2)
  A->>B: Send Confirmation (retry 2)
  B-->>A: 200 OK - Sent

หลักการสำคัญ:

  • Exponential Backoff: เว้นระยะ retry แบบเพิ่มขึ้นเรื่อย ๆ เช่น 500ms → 1s → 2s → 4s ไม่ retry ทันทีซ้ำ ๆ
  • Jitter: เพิ่ม random delay เล็กน้อยเข้าไป เพื่อป้องกัน thundering herd problem (ทุก instance retry พร้อมกัน)
  • Max Attempts: กำหนดจำนวน retry สูงสุด ไม่ retry ไปเรื่อย ๆ
  • Retry only on transient errors: retry เฉพาะ error ที่มีโอกาสหายเอง (5xx, timeout) ไม่ retry 400 Bad Request เพราะ retry กี่ทีก็ fail เหมือนเดิม

Retry กับ Circuit Breaker ควรใช้คู่กัน — Retry ช่วยกรณี transient failure, แต่ถ้า failure ยาวนาน Circuit Breaker จะตัดวงจรเพื่อไม่ให้ Retry ยิง request ไปเรื่อย ๆ

8.3 Rate Limiting

Rate Limiting ควบคุมจำนวน request ที่ service รับได้ในช่วงเวลาหนึ่ง เพื่อป้องกัน service ถูก overload ไม่ว่าจะจาก traffic spike ปกติหรือ abuse

flowchart LR
  C1[Client A] --> RL[Rate Limiter
100 req/s per client]
  C2[Client B] --> RL
  C3[Client C] --> RL
  RL -->|allowed| SVC[Booking Service]
  RL -->|exceeded| ERR[429 Too Many
Requests]

ตัวอย่าง: Public API ของระบบ logistics เปิดให้ partner เรียก tracking API — กำหนดไว้ที่ 100 requests/วินาที ต่อ API key ถ้าเกินก็ตอบ HTTP 429 Too Many Requests กลับไป

Rate Limiting ทำได้หลายระดับ:

  • API Gateway level: กำหนด global rate limit ก่อนที่ request จะเข้าถึง service (เหมาะสำหรับป้องกัน abuse)
  • Service level: แต่ละ service กำหนด rate limit ของตัวเอง (เหมาะสำหรับ protect resource-intensive operations)
  • Per-client level: rate limit แยกตาม API key, user ID, หรือ IP

8.4 Timeout

Timeout กำหนดเวลาสูงสุดที่ service จะรอ response จาก downstream ถ้าเกินเวลาที่กำหนดก็ตัด request ทิ้งทันที ไม่ปล่อยให้ thread ค้างรอไปเรื่อย ๆ

ตัวอย่าง: Booking Service เรียก External Calendar API เพื่อเช็คตารางว่าง — กำหนด timeout ไว้ 3 วินาที ถ้า Calendar API ตอบช้ากว่านั้นก็ตัดทิ้งแล้ว fallback ไปใช้ cached schedule แทน

Timeout ที่ดีต้องตั้งให้เหมาะสมกับแต่ละ downstream ไม่ใช่ใช้ค่าเดียวกันหมด service ที่ทำ heavy computation อาจต้องการ timeout ที่นานกว่า service ที่แค่ดึงข้อมูล

8.5 Bulkhead

Bulkhead ยืมแนวคิดมาจากผนังกั้นน้ำในเรือ — ถ้าเรือรั่วที่ห้องหนึ่ง น้ำจะท่วมแค่ห้องนั้น ไม่ลามไปห้องอื่น

ในระบบ Microservices Bulkhead แบ่ง resource (เช่น thread pool, connection pool) ออกเป็นส่วน ๆ แยกตาม downstream service ถ้า downstream ตัวหนึ่งช้าหรือล่ม ก็กิน resource แค่ส่วนที่แบ่งไว้ให้ ไม่กระทบ downstream ตัวอื่น

flowchart TB
  SVC[Service A]
  SVC --> P1[Thread Pool 1
10 threads]
  SVC --> P2[Thread Pool 2
10 threads]
  SVC --> P3[Thread Pool 3
10 threads]
  P1 --> D1[Service B]
  P2 --> D2[Service C]
  P3 --> D3[Service D]

ตัวอย่าง: Notification Service เรียก SMS Provider, Email Provider, และ Push Notification Provider — แต่ละ provider ใช้ thread pool แยกกัน ถ้า SMS Provider ช้า ก็กิน thread แค่ pool ของตัวเอง Email กับ Push ยังทำงานปกติ

Resilience Patterns ทำงานร่วมกัน

patterns เหล่านี้ควรใช้ร่วมกัน ไม่ใช่เลือกแค่ตัวใดตัวหนึ่ง

flowchart LR
  REQ[Incoming
Request] --> RL[Rate Limiter]
  RL --> BH[Bulkhead]
  BH --> TM[Timeout]
  TM --> CB[Circuit Breaker]
  CB --> RT[Retry]
  RT --> SVC[Downstream
Service]

ลำดับการทำงานที่แนะนำ:

  1. Rate Limiter กรอง request ที่เกิน quota ออกก่อน
  2. Bulkhead จำกัด resource ที่แต่ละ downstream ใช้ได้
  3. Timeout กำหนดเวลาสูงสุดที่จะรอ response
  4. Circuit Breaker เช็คว่า downstream ยังปกติหรือ circuit open อยู่
  5. Retry ถ้า request fail ด้วย transient error ก็ลองใหม่ตาม strategy ที่ตั้งไว้

9. Externalized Configuration

เมื่อ service ถูก scale ออกไป 50-100 instances ถ้า config ถูก bundle ไปกับ code ทุกครั้งที่เปลี่ยน config ต้อง redeploy ทุก instance ซึ่งเสียเวลาและเสี่ยงต่อ inconsistent state (บาง instance ได้ config ใหม่ บางตัวยังใช้ config เก่า)

flowchart LR
  subgraph Instances
      I1[Instance 1]
      I2[Instance 2]
      I3[Instance N]
  end
  CS[(Config Store
e.g. Consul, etcd,
Spring Cloud Config)]
  I1 --> CS
  I2 --> CS
  I3 --> CS
  CS --> DB[(Database)]
  CS --> ENV[Environment
Variables]

แนวทางคือแยก configuration ออกมาเก็บใน external store เช่น HashiCorp Consul, etcd, Spring Cloud Config Server, หรือแม้แต่ environment variables ที่จัดการผ่าน Kubernetes ConfigMap

เมื่อ service start ขึ้นมาก็ดึง config จาก external store ถ้า config เปลี่ยนระหว่าง runtime service ก็ reload config ใหม่ได้โดยไม่ต้อง redeploy


10. Consumer-Driven Contract Testing (CDCT)

เมื่อ service A (consumer) เรียกใช้ service B (provider) สิ่งที่มักเกิดขึ้นคือ provider เปลี่ยน API response format แล้ว consumer พัง เพราะไม่มีใครรู้ว่า consumer คาดหวัง response แบบไหน

CDCT แก้ปัญหานี้โดยให้ consumer เป็นคนเขียน “contract” ระบุว่าตัวเองคาดหวัง request/response format แบบไหน แล้ว provider เอา contract นี้ไปรันเป็น test

flowchart LR
  CON[Consumer Team] -->|writes| CT[Contract
JSON/Pact file]
  CT -->|shared with| PRO[Provider Team]
  PRO -->|runs as test suite| TEST[Provider Tests]
  TEST -->|pass/fail| RESULT[Integration Verified]

ตัวอย่าง: Mobile App (consumer) เรียก User Profile API (provider) — Mobile team เขียน contract ว่า “ผมคาดหวังว่า GET /users/123 จะ return JSON ที่มี field: name (string), email (string), avatar_url (string)” ถ้า Provider team จะเปลี่ยน field name เป็น full_name contract test จะ fail ก่อนที่จะ deploy ขึ้น production

ข้อดี:

  • ป้องกัน integration issue ก่อน deploy
  • provider รู้ว่า consumer ใช้ field อะไรบ้าง ถ้าจะเปลี่ยนก็คุยกันก่อนได้
  • ลดจำนวน end-to-end test ที่ต้องรัน

Tools ที่ใช้กันเช่น Pact, Spring Cloud Contract


11. Caching Patterns

Caching เป็นอีก pattern ที่ใช้กันมากใน Microservices เพื่อลด load ที่ database และเพิ่ม response time ให้เร็วขึ้น แต่วิธีการจัดการ cache มีหลายแบบ แต่ละแบบเหมาะกับ use case ที่ต่างกัน

11.1 Cache-Aside (Lazy Loading)

Application เป็นคนจัดการ cache เอง — เช็ค cache ก่อน ถ้าไม่เจอ (cache miss) ก็ไปดึงจาก database แล้วเอามาเก็บใน cache เพื่อให้ request ถัดไปใช้ได้

sequenceDiagram
  participant App as Application
  participant Cache as Cache (Redis)
  participant DB as Database

  App->>Cache: GET flight:BKK-NRT-20260501
  alt Cache Hit
      Cache-->>App: Return cached data
  else Cache Miss
      Cache-->>App: null
      App->>DB: Query flight schedule
      DB-->>App: Flight data
      App->>Cache: SET flight:BKK-NRT-20260501 (with TTL)
      App-->>App: Return data
  end

ตัวอย่าง: Flight Search Page — user ค้นหาเที่ยวบิน BKK-NRT application เช็ค Redis ก่อน ถ้ามีก็ return ทันที ถ้าไม่มีก็ query จาก database แล้วเก็บลง Redis พร้อมตั้ง TTL (Time-To-Live) ไว้ เช่น 5 นาที

ข้อดี: เก็บ cache เฉพาะข้อมูลที่ถูกเรียกใช้ ไม่เปลือง memory กับข้อมูลที่ไม่มีใครใช้

ข้อเสีย: request แรกจะช้าเสมอ (cache miss) และ application ต้องจัดการ logic cache เอง

11.2 Read-Through

คล้าย Cache-Aside แต่ต่างตรงที่ cache layer เป็นคนไปดึงข้อมูลจาก database เอง application แค่ request ไปที่ cache ไม่ต้องรู้ว่า data มาจาก cache หรือ database

sequenceDiagram
  participant App as Application
  participant Cache as Cache Layer
  participant DB as Database

  App->>Cache: GET user:456
  alt Cache Hit
      Cache-->>App: Return cached data
  else Cache Miss
      Cache->>DB: Fetch from DB
      DB-->>Cache: User profile data
      Cache->>Cache: Store in cache
      Cache-->>App: Return data
  end

ตัวอย่าง: User Profile Service — ทุก request ที่ขอข้อมูล user จะเรียกผ่าน cache layer ตัวเดียว ถ้า cache miss ตัว cache layer จะไปดึงจาก database เอง application ไม่ต้องเขียน logic จัดการ cache/database เอง

ข้อดี: application code สะอาดขึ้น ไม่ต้อง handle cache miss logic เอง

ข้อเสีย: cache layer ต้องรู้จัก data model และวิธี query database ซึ่งเพิ่มความซับซ้อนของ cache layer

11.3 Write-Through

เมื่อ application เขียนข้อมูล ข้อมูลจะถูกเขียนลง cache และ database พร้อมกัน (synchronous) ทำให้ cache กับ database มีข้อมูลตรงกันตลอด

sequenceDiagram
  participant App as Application
  participant Cache as Cache Layer
  participant DB as Database

  App->>Cache: Write seat:A12-status
  Cache->>DB: Write to DB (sync)
  DB-->>Cache: Write confirmed
  Cache-->>App: Write confirmed
  Note over Cache,DB: Cache and DB<br/>always in sync

ตัวอย่าง: ระบบ Seat Reservation ของสายการบินที่ต้องการสถานะที่นั่งที่ถูกต้องเสมอ — เมื่ออัปเดตสถานะที่นั่ง ข้อมูลจะถูกเขียนทั้ง cache และ database พร้อมกัน ทำให้ทุก read ที่มาจาก cache ได้ข้อมูลที่ตรงกับ database

ข้อดี: ไม่มีปัญหา stale data เพราะ cache กับ database sync กันตลอด

ข้อเสีย: write ช้าขึ้นเพราะต้องเขียน 2 ที่ และอาจเก็บ data ใน cache ที่ไม่มีใครมาอ่าน

เลือก Caching Pattern ไหนดี?

Patternเหมาะกับข้อควรระวัง
Cache-AsideRead-heavy, ข้อมูลที่ไม่ได้เปลี่ยนบ่อยRequest แรกช้า, app ต้องจัดการ cache เอง
Read-Throughต้องการ abstraction ระหว่าง app กับ data sourceCache layer ซับซ้อนขึ้น
Write-Throughต้องการ data consistency สูงWrite latency เพิ่มขึ้น

ในทางปฏิบัติหลายระบบใช้ pattern เหล่านี้ร่วมกัน เช่น Read-Through + Write-Through เพื่อให้ทั้ง read และ write ผ่าน cache layer เดียวกัน ได้ทั้ง performance และ consistency


สรุป

ทั้ง 11 patterns นี้ไม่ได้ต้องใช้ทั้งหมดในทุกระบบ แต่ละ pattern แก้ปัญหาเฉพาะทาง:

Patternแก้ปัญหาเรื่อง
Database per MicroserviceService independence & data isolation
CQRSRead/write performance optimization
Event SourcingData consistency & audit trail
SagaDistributed transaction management
BFFFrontend data aggregation
API GatewaySingle entry point & cross-cutting concerns
StranglerMonolith-to-microservices migration
Resilience (Circuit Breaker, Rate Limit, Retry, Timeout, Bulkhead)Fault tolerance & cascading failure prevention
Externalized ConfigurationConfig management at scale
CDCTAPI contract & integration safety
Caching PatternsPerformance optimization & database load reduction

ผมมองว่าจุดเริ่มต้นที่ดีคือ Database per Microservice กับ API Gateway เพราะเป็น foundation ที่ pattern อื่น ๆ ต่อยอดได้ จากนั้นค่อยเพิ่ม pattern อื่น ๆ ตามปัญหาที่เจอ ไม่ต้องออกแบบ over-engineer ตั้งแต่แรก