
Microservice Design Patterns which you'll face (Now or Future)
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 ทำ:
- added item A
- changed quantity of item A to 3
- removed item B
- 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 แล้วปิดตัวลง
ขั้นตอนหลัก ๆ:
- เลือก module ที่จะแยกออกมาโดยยึดตาม business domain
- สร้าง Microservice ใหม่สำหรับ module นั้น
- ใช้ API Gateway route traffic บางส่วนไปที่ service ใหม่
- ค่อย ๆ เพิ่ม traffic จนครบ 100%
- 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]
ลำดับการทำงานที่แนะนำ:
- Rate Limiter กรอง request ที่เกิน quota ออกก่อน
- Bulkhead จำกัด resource ที่แต่ละ downstream ใช้ได้
- Timeout กำหนดเวลาสูงสุดที่จะรอ response
- Circuit Breaker เช็คว่า downstream ยังปกติหรือ circuit open อยู่
- 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-Aside | Read-heavy, ข้อมูลที่ไม่ได้เปลี่ยนบ่อย | Request แรกช้า, app ต้องจัดการ cache เอง |
| Read-Through | ต้องการ abstraction ระหว่าง app กับ data source | Cache layer ซับซ้อนขึ้น |
| Write-Through | ต้องการ data consistency สูง | Write latency เพิ่มขึ้น |
ในทางปฏิบัติหลายระบบใช้ pattern เหล่านี้ร่วมกัน เช่น Read-Through + Write-Through เพื่อให้ทั้ง read และ write ผ่าน cache layer เดียวกัน ได้ทั้ง performance และ consistency
สรุป
ทั้ง 11 patterns นี้ไม่ได้ต้องใช้ทั้งหมดในทุกระบบ แต่ละ pattern แก้ปัญหาเฉพาะทาง:
| Pattern | แก้ปัญหาเรื่อง |
|---|---|
| Database per Microservice | Service independence & data isolation |
| CQRS | Read/write performance optimization |
| Event Sourcing | Data consistency & audit trail |
| Saga | Distributed transaction management |
| BFF | Frontend data aggregation |
| API Gateway | Single entry point & cross-cutting concerns |
| Strangler | Monolith-to-microservices migration |
| Resilience (Circuit Breaker, Rate Limit, Retry, Timeout, Bulkhead) | Fault tolerance & cascading failure prevention |
| Externalized Configuration | Config management at scale |
| CDCT | API contract & integration safety |
| Caching Patterns | Performance optimization & database load reduction |
ผมมองว่าจุดเริ่มต้นที่ดีคือ Database per Microservice กับ API Gateway เพราะเป็น foundation ที่ pattern อื่น ๆ ต่อยอดได้ จากนั้นค่อยเพิ่ม pattern อื่น ๆ ตามปัญหาที่เจอ ไม่ต้องออกแบบ over-engineer ตั้งแต่แรก