Network Computing · แล็ปโน้ต 4 จาก 5

Microservice Patterns

Pattern มีจำนวนมาก แต่ถ้าแบ่งตาม "ปัญหาที่ pattern พยายามแก้" จะเข้าใจง่ายกว่าจำรายชื่อ — จัดเป็น 8 กลุ่มใหญ่ ตั้งแต่การแบ่ง service ไปจนถึงการย้ายระบบเก่า พร้อม anti-pattern ที่ควรรู้คู่กันเพื่อไม่ใช้ microservices โดยไม่เข้าใจต้นทุน

🔗
ต่อยอดจาก: หัวข้อที่ 2 — Network Services (REST, API contract, service ที่เป็นทั้ง client และ server) และ หัวข้อที่ 3 — Failure, Deployment & Observation (timeout, retry, idempotency, health check, correlation ID, Docker networking) — pattern หลายตัวในหน้านี้สร้างต่อยอดจากกลไกที่เรียนไปแล้วโดยตรง

1Service Decomposition Patterns

ใช้ตอบคำถามว่า "จะแบ่งระบบออกเป็น service อย่างไร"

Decompose by Business Capability
แบ่งตามความสามารถทางธุรกิจ เช่น Customer Service, Order Service, Payment Service, Inventory Service — แต่ละ service มีความรับผิดชอบชัดเจนและพัฒนาแยกกันได้ เป็นวิธีแบ่งที่เข้าใจง่ายที่สุดสำหรับคนที่คุ้นกับ organizational structure ของธุรกิจอยู่แล้ว
Decompose by Subdomain (Domain-Driven Design)
แบ่งตามแนวคิด DDD เช่น Ordering Domain, Billing Domain, Shipping Domain — แต่ละ subdomain มีแบบจำลอง (model) และคำศัพท์ (ubiquitous language) ของตนเอง ต่างจาก business capability ตรงที่เน้นขอบเขตของ "ความหมาย" มากกว่าขอบเขตขององค์กร บางครั้งสองวิธีให้ผลแบ่งที่ใกล้เคียงกัน แต่ไม่เสมอไป
Single Responsibility per Service
หนึ่ง service ควรมีเหตุผลหลักเพียงเรื่องเดียวที่ทำให้ต้องเปลี่ยน — ไม่ใช่แตกทุก function ให้กลายเป็น service เล็กมากโดยอัตโนมัติ (ดู Nano-services ในหัวข้อ Anti-pattern ท้ายบทความ) ขนาดที่เหมาะสมของ service วัดจาก "ขอบเขตความรับผิดชอบ" ไม่ใช่จำนวนบรรทัดโค้ด

Strangler Fig Pattern

ใช้ย้ายระบบเก่าไปสู่ microservices ทีละส่วน
ชื่อมาจากต้นไม้ Strangler Fig ที่ขึ้นพันต้นไม้เดิมแล้วค่อยๆ แทนที่ — สร้าง service ใหม่ทีละส่วน แล้วเปลี่ยนเส้นทางจากระบบเก่ามายัง service ใหม่ทีละ module โดยที่ระบบเก่ายังทำงานคู่ขนานไปกับส่วนที่ยังไม่ได้ย้าย
ก่อนย้าย:        Client → [ ระบบเก่าทั้งก้อน (Monolith / ERP) ]

ระหว่างย้าย:      Client → [ Router/Proxy ] → module A → ระบบเก่า (ยังไม่ย้าย)
                                            → module B → Service ใหม่ (ย้ายแล้ว)

หลังย้ายครบ:      Client → [ Router/Proxy ] → Service A, B, C, ... (ระบบเก่าถูกถอดออก)

เมื่อสร้าง service ใหม่เสร็จ ก็เปลี่ยนเส้นทางจากระบบเก่ามายัง service ใหม่ทีละ module — เหมาะกับระบบ ERP เก่าที่มีอยู่แล้วมากกว่าการเขียนใหม่ทั้งหมดในครั้งเดียว เพราะลดความเสี่ยง (ไม่ต้อง "big bang" เปลี่ยนทั้งระบบพร้อมกัน) และยังให้ธุรกิจใช้งานระบบได้ต่อเนื่องระหว่างย้าย

ข้อควรจำสำหรับสอบ
Strangler Fig เป็น pattern ระดับ "กลยุทธ์การย้ายระบบ" เชื่อมโยงกับ Migration Patterns ในหัวข้อ 8 โดยตรง (Anti-Corruption Layer และ Parallel Run มักถูกใช้ร่วมกันระหว่างการ strangle)

2Communication Patterns

ใช้กำหนดว่า service จะสื่อสารกันอย่างไร

Request–Response
ผู้ส่งรอคำตอบจากผู้รับ เช่น REST หรือ RPC (ดู หัวข้อที่ 2 เรื่อง Synchronous Communication) — เข้าใจง่าย แต่ทำให้ service ผูกกับความพร้อมของอีก service โดยตรง ถ้าปลายทางช้าหรือล่ม ผู้เรียกก็ค้าง/ล้มเหลวตามไปด้วย
Service A  ──── request ────►  Service B
Service A  ◄─── response ────  Service B
(A บล็อกรอจนกว่า B จะตอบ หรือ timeout)
Asynchronous Messaging
ส่ง message แล้วไม่ต้องรอให้อีกฝ่ายประมวลผลทันที — รูปแบบหลัก: Message Queue, Event, Publish–Subscribe ช่วยลดการผูกกันโดยตรง (decoupling) แต่ต้องรับมือกับ message ซ้ำ, ลำดับข้อความที่อาจไม่เรียง, และ eventual consistency (รายละเอียดในหัวข้อ 5)
Remote Procedure Invocation (RPI)
ทำให้การเรียก service ดูคล้ายเรียก function ธรรมดา เช่น gRPC — แต่ต้องระวังว่า remote call ยังมี latency และล้มเหลวได้ต่างจาก local function call (local function ไม่มี network ระหว่างทาง ไม่มี partial failure) การซ่อน network ไว้หลัง syntax ที่ดูเหมือน local call มีความเสี่ยงที่ทำให้ผู้พัฒนาลืมออกแบบรับมือ failure

API Gateway

ให้ client ติดต่อผ่านทางเข้ากลาง
Client ไม่เรียก service ภายในโดยตรง แต่เรียกผ่าน gateway ตัวเดียว — Gateway อาจดูแล routing, authentication, rate limiting และรวมคำตอบจากหลาย service ก่อนส่งกลับ
                        ┌──► Order Service
Client ──► API Gateway ─┼──► Payment Service
                        └──► Inventory Service
(routing / authentication / rate limiting / response aggregation)

Backend for Frontend (BFF)

สร้าง backend แยกตาม client
เช่น Web BFF, Mobile BFF, Partner BFF — แต่ละ client จึงไม่ต้องรับ API ที่กว้างหรือซับซ้อนเกินความต้องการของตัวเอง (เช่น mobile ต้องการ payload เล็กกว่า web) BFF คือ API Gateway ที่ specialize ตาม client แทนที่จะใช้ gateway เดียวหน้าตาเดียวกันสำหรับทุก client
Service Discovery
Service ค้นหาที่อยู่ของ service อื่นโดยใช้ชื่อ แทนการ hard-code IP address — กลไกเดียวกับที่ใช้จริงแล้วใน Docker Networking และ Service Name (หัวข้อที่ 3) ที่ container เรียกหากันด้วยชื่อ service ผ่าน DNS ภายใน
Aggregator Pattern
Service หนึ่งเรียกหลาย service แล้วรวมผลกลับไปยัง client (คล้ายบทบาทหนึ่งของ API Gateway แต่เน้นการรวมข้อมูลโดยเฉพาะ) — ข้อเสีย: ถ้าเรียกหลายบริการพร้อมกัน tail latency (เวลาตอบสนองของ request ที่ช้าที่สุดในกลุ่ม) และโอกาสล้มเหลวจะเพิ่มขึ้น เพราะต้องรอ service ที่ช้าที่สุดในกลุ่มเสมอ

3Data Management Patterns

ใช้ตอบปัญหาว่าแต่ละ service จะจัดการข้อมูลร่วมกันอย่างไร

Database per Service
แต่ละ service เป็นเจ้าของข้อมูลของตนเอง — service อื่นไม่ควรข้ามไปอ่านตารางโดยตรง แต่เรียกผ่าน API หรือรับ event แทน
Order Service → DB(orders)
Payment Service → DB(payments)
Inventory Service → DB(inventory)
Shared Database
หลาย service ใช้ฐานข้อมูลเดียวกัน — ทำง่ายในช่วงเริ่มต้นหรือช่วง migration แต่เกิด coupling ผ่าน schema และ transaction ได้ง่าย เปลี่ยน schema ของตารางหนึ่งอาจกระทบ service อื่นที่ไม่รู้ตัว (ดู Shared Database Everywhere ใน Anti-pattern)

Saga Pattern

จัดการ transaction ข้ามหลาย service ด้วยชุด local transactions
เพราะแต่ละ service มีฐานข้อมูลของตัวเอง (Database per Service) จึงไม่มี distributed transaction แบบ 2-phase commit ข้ามทุก service ได้ง่าย Saga แก้ด้วยการแตก transaction ใหญ่เป็นชุด local transaction ที่ต่อกันเป็นทอด
ตัวอย่าง: สั่งซื้อสินค้า
1. Order      สร้างคำสั่งซื้อ
2. Payment    ตัดเงิน
3. Inventory  ตัดสต็อก
4. Shipping   เตรียมจัดส่ง
ถ้าขั้นที่ 3 (Inventory) ล้มเหลว ต้องมี compensating action เช่น คืนเงินที่ตัดไปแล้วในขั้นที่ 2 — ไม่ใช่ใช้ rollback ข้ามทุก service แบบฐานข้อมูลเดียว เพราะไม่มีกลไก rollback ข้าม service ให้ใช้ ต้อง "แก้ไขย้อนหลัง" ด้วย transaction ชดเชยแทน
รูปแบบ Sagaวิธีทำงาน
Choreographyแต่ละ service ตอบสนองต่อ event ของ service ก่อนหน้าเอง ไม่มีตัวกลางควบคุม — ง่ายสำหรับ saga สั้นๆ แต่ตามลำดับ/debug ยากขึ้นเมื่อ step เยอะ
Orchestrationมีตัวกลาง (orchestrator) ควบคุมลำดับงานทั้งหมดอย่างชัดเจน — เห็นภาพรวมง่ายกว่า แต่ orchestrator กลายเป็นจุดที่ตรรกะสำคัญกระจุกตัว

Event Sourcing

เก็บเหตุการณ์ ไม่ใช่แค่สถานะล่าสุด
แทนที่จะเก็บเฉพาะสถานะปัจจุบันของ record (แบบ CRUD ทั่วไป) Event Sourcing เก็บลำดับเหตุการณ์ที่ทำให้สถานะเปลี่ยนทั้งหมด สถานะปัจจุบันคำนวณได้จากการ "replay" เหตุการณ์ทั้งหมดตามลำดับ
OrderCreated
ItemAdded
PaymentConfirmed
OrderCancelled

มีประโยชน์ด้าน audit (รู้ว่าเกิดอะไรขึ้นบ้างตามลำดับเวลา) และ reconstruction (คำนวณสถานะ ณ จุดเวลาใดก็ได้ย้อนหลัง) แต่ซับซ้อนกว่า CRUD มาก — ต้องออกแบบวิธี replay, versioning ของ event schema, และมักต้องมี snapshot เพื่อไม่ต้อง replay ตั้งแต่ต้นทุกครั้ง

CQRS (Command Query Responsibility Segregation)

แยกแบบจำลองสำหรับการเขียนและการอ่าน
Command: เปลี่ยนสถานะ (write) — Query: อ่านข้อมูล (read) สองเส้นทางนี้ใช้ model ต่างกันได้ (เช่น write model normalize เต็มที่ ส่วน read model denormalize เพื่อ query เร็ว) มักใช้ร่วมกับ Event Sourcing (event เป็นแหล่งความจริงฝั่ง write, read model สร้างจาก event เหล่านั้น) แต่ไม่จำเป็นต้องใช้ร่วมกันเสมอ — CQRS ใช้ได้แม้ไม่มี Event Sourcing

Transactional Outbox

ปัญหาที่แก้: ฐานข้อมูลบันทึกสำเร็จ แต่ระบบล้มก่อนส่ง event
บันทึกการเปลี่ยนฐานข้อมูลและ event ที่ต้องส่งไว้ใน local transaction เดียวกัน (เช่น เขียนแถวใหม่ลงตาราง outbox พร้อมกับข้อมูลจริงในธุรกรรมเดียว) แล้วค่อยมี process แยกต่างหาก (relay/poller) อ่านจากตาราง outbox แล้วส่ง event ออกไปจริง — รับประกันว่าถ้าข้อมูลถูกบันทึกสำเร็จ event ก็จะถูกส่งในที่สุด (ไม่มีเคส "DB เขียนสำเร็จแต่ event หายไปเฉยๆ")
Idempotent Consumer
Consumer ต้องรับ message เดิมซ้ำแล้วไม่ทำให้ผลลัพธ์ซ้ำ เช่น ไม่ตัดเงินสองครั้งเพราะได้รับ event ซ้ำ — แนวคิดเดียวกับ Idempotency Key ที่เรียนไปแล้วในหัวข้อที่ 3 เพียงแต่คราวนี้ฝั่งที่ต้องรับผิดชอบคือ consumer ของ message ไม่ใช่ server ที่รับ HTTP request โดยตรง

4Resilience Patterns

ใช้รับมือ service ช้า ล่ม หรือ network ขาดช่วง

Timeout · Retry · Exponential Backoff
สามกลไกพื้นฐานที่เรียนไปแล้วแบบละเอียดใน หัวข้อที่ 3: กำหนดเวลารอสูงสุด (timeout), ลองใหม่เมื่อ error น่าจะเป็นชั่วคราว โดยควรใช้เฉพาะ operation ที่ปลอดภัยต่อการทำซ้ำหรือมี idempotency key (retry), และเพิ่มเวลารอก่อน retry แต่ละครั้งแบบเอกซ์โปเนนเชียลพร้อม jitter (exponential backoff)
รูปแบบ Backoff ที่พบบ่อย
1s → 2s → 4s → 8s → ...
โดยทั่วไปควรเติม jitter (สุ่มเวลารอเล็กน้อย) เพื่อไม่ให้ client จำนวนมาก retry พร้อมกันเป๊ะ (ดูรายละเอียดสูตรและโค้ดใน หัวข้อที่ 3)

Circuit Breaker

หยุดเรียก service ที่ล้มเหลวต่อเนื่องชั่วคราว
ทำงานคล้ายเบรกเกอร์ไฟฟ้า — ป้องกันความล้มเหลวลุกลามไปทั้งระบบ (ถ้าปล่อยให้ทุก request ยังพยายามเรียก service ที่ล่มอยู่ ทั้ง caller และ callee จะยิ่งแย่ลง)
สถานะพฤติกรรม
Closedเรียกตามปกติ — นับจำนวน error ที่เกิดขึ้น ถ้าเกิน threshold ที่กำหนด เปลี่ยนไปเป็น Open
Openปฏิเสธ request ทันทีโดยไม่ยิงไปหา service ปลายทางเลย (fail fast) — รอครบเวลาที่กำหนดแล้วเปลี่ยนไปเป็น Half-open
Half-openทดลองเรียกบาง request เพื่อดูว่า service ฟื้นแล้วหรือยัง — สำเร็จมากพอ กลับไปเป็น Closed, ล้มเหลวอีก กลับไปเป็น Open
เชื่อมโยงกับ Retry Storm
Circuit Breaker คือกลไกหลักที่ป้องกัน Retry Storm (ดู Anti-pattern ท้ายบทความ) — ถ้าไม่มี circuit breaker ทุก client ที่ retry พร้อมกันจะยิงไปหา service ที่กำลังพยายามฟื้นตัวซ้ำแล้วซ้ำเล่า
Bulkhead
แยกทรัพยากรออกเป็นส่วนๆ เช่น thread pool หรือ connection pool แยกตาม downstream service เพื่อให้ service หนึ่งล่มแล้วไม่ใช้ทรัพยากรทั้งหมด — ชื่อมาจากผนังกั้นห้องในเรือ (bulkhead) ที่ป้องกันน้ำท่วมห้องหนึ่งแล้วลามไปทั้งลำเรือ ถ้าไม่มี bulkhead การเรียก service ที่ช้าตัวเดียวอาจทำให้ thread ทั้งหมดของระบบติดค้างรอ จนบริการอื่นที่ไม่เกี่ยวข้องพลอยใช้งานไม่ได้ไปด้วย
Rate Limiting
จำกัดจำนวน request เพื่อป้องกัน overload หรือ abuse — ปกป้อง service จากผู้เรียกฝั่งเดียวกันหรือ client ที่ผิดปกติ
Load Shedding
เมื่อระบบเกินกำลัง ให้ปฏิเสธงานบางส่วนอย่างควบคุมได้ แทนที่จะรับทั้งหมดแล้วล่มพร้อมกัน — ต่างจาก rate limiting ตรงที่ rate limiting ป้องกันล่วงหน้าตามโควตา ส่วน load shedding ตัดสินใจ ณ ขณะที่ระบบเริ่มโอเวอร์โหลดจริง
Fallback
ใช้คำตอบสำรอง เช่น cached data หรือความสามารถแบบลดรูป (degraded functionality) เมื่อ service หลักเรียกไม่สำเร็จ — ข้อควรระวัง: ต้องไม่ให้ fallback ซ่อนข้อผิดพลาดจนผู้ใช้/ทีมไม่รู้ว่าระบบจริงกำลังมีปัญหาอยู่ (เช่น ถ้า fallback ทำงานเงียบๆ ตลอดโดยไม่มี log/alert ทีมอาจไม่รู้เลยว่า service หลักล่มมาหลายวันแล้ว)
Health Check
แยกได้เป็น Liveness (process ยังมีชีวิตหรือไม่) และ Readiness (พร้อมรับงานจริงหรือไม่) — อธิบายละเอียดพร้อมตัวอย่างแล้วใน หัวข้อที่ 3

5Consistency and Messaging Patterns

Eventual Consistency
ยอมให้แต่ละ service เห็นข้อมูลไม่ตรงกันชั่วคราว แต่ต้องมีกลไกทำให้สอดคล้องกันในภายหลัง — เป็นผลที่ตามมาโดยธรรมชาติจากการใช้ Database per Service ร่วมกับ Asynchronous Messaging: ไม่มีทางทำให้ทุก service เห็นข้อมูลอัปเดตพร้อมกันแบบ instant ได้ (ต่างจากฐานข้อมูลเดียวที่ transaction เดียวอัปเดตทุกอย่างพร้อมกัน)
At-least-once Delivery
Message อาจถูกส่งซ้ำ แต่ไม่ควรสูญหายง่าย — เป็น trade-off ที่ message queue ส่วนใหญ่เลือก (แลก "อาจซ้ำ" กับ "ไม่มีทางหาย") ดังนั้น consumer ต้องรองรับ duplicate เสมอ ผ่านการออกแบบเป็น Idempotent Consumer (หัวข้อ 3)
Dead Letter Queue (DLQ)
เก็บ message ที่ประมวลผลไม่สำเร็จหลายครั้ง (เกิน retry limit) เพื่อวิเคราะห์หรือประมวลผลใหม่ภายหลัง แทนที่จะปล่อยให้หายไปเฉยๆ หรือ block queue หลักไว้
Competing Consumers
ให้ worker หลายตัวอ่านงานจาก queue เดียวกัน เพื่อกระจายภาระ (แต่ละ message ถูกประมวลผลโดย worker เพียงตัวเดียว ไม่ใช่ทุกตัว — ต่างจาก Publish-Subscribe ที่ทุก subscriber ได้รับ message เดียวกัน)
Correlation ID
ใส่รหัสเดียวกันตลอดสายของ request หรือ workflow เพื่อเชื่อม log จากหลาย service เข้าด้วยกัน — อธิบายละเอียดพร้อมโค้ดตัวอย่างแล้วใน หัวข้อที่ 3

6Deployment Patterns

Service Instance per Container
แต่ละ container รัน service instance เดียว ทำให้ deploy และ scale แยกกันได้อิสระต่อ service — เป็นฐานของแนวคิด Docker/Kubernetes ที่ใช้ใน หัวข้อที่ 3
Sidecar Pattern
วาง component สนับสนุนข้าง service เช่น proxy, log collector, security agent โดยไม่ต้องฝังไว้ใน application โดยตรง — แยกความรับผิดชอบ (cross-cutting concern) ออกจาก business logic ของ service หลัก
รูปแบบ Deployวิธีทำงาน
Blue–Green Deploymentมีระบบเก่า (blue) และระบบใหม่ (green) ทำงานคู่กันเต็มรูปแบบ แล้วสลับ traffic ทั้งหมดเมื่อระบบใหม่พร้อม — สลับกลับ (rollback) ได้เร็วถ้าเจอปัญหา
Canary Releaseส่ง traffic ส่วนน้อยไปยัง version ใหม่ก่อน แล้วค่อยเพิ่มสัดส่วนถ้าผลปกติ — ความเสี่ยงจำกัดกว่า blue-green เพราะผู้ใช้ที่กระทบมีจำนวนน้อยในช่วงแรก
Rolling Deploymentเปลี่ยน instance จาก version เก่าเป็น version ใหม่ทีละชุด (ไม่ใช่ทั้งหมดพร้อมกัน) — ระบบยังให้บริการต่อเนื่องระหว่าง deploy แต่จะมีทั้งสอง version รันพร้อมกันชั่วขณะ (ต้องออกแบบให้ backward-compatible)
Externalized Configuration
แยก configuration ออกจาก source code เช่น endpoint, credential reference, และ environment-specific settings — เปลี่ยนค่า config ได้โดยไม่ต้อง build/deploy โค้ดใหม่ และไม่ทำให้ secret หลุดไปอยู่ใน source control

7Observability Patterns

ทำไมต้องมีหัวข้อนี้แยก
ระบบ microservices ดูจาก log ของโปรแกรมเดียวไม่พอ เพราะ request เดินทางผ่านหลาย service — ถ้าไม่มีเครื่องมือ observability ที่ออกแบบมาสำหรับ distributed system โดยเฉพาะ การ debug ปัญหาข้าม service จะแทบเป็นไปไม่ได้เมื่อระบบมี traffic พร้อมกันจำนวนมาก
Patternใช้ทำอะไร
Centralized Loggingรวบรวม log จากทุก service มาไว้ในระบบกลาง — ค้นหาและกรองข้ามหลาย service ได้จากที่เดียว (ทำงานร่วมกับ Correlation ID ในหัวข้อ 5)
Distributed Tracingติดตาม request ตั้งแต่ต้นจนจบว่าใช้เวลากับ service ใดบ้าง — เห็นภาพรวมของ latency ตลอด call chain ทั้งสาย ไม่ใช่แค่ทีละ service
Metrics and Alertingติดตามตัวชี้วัดเชิงระบบ เช่น request rate, error rate, response time, saturation พร้อมตั้งเงื่อนไขแจ้งเตือนอัตโนมัติเมื่อค่าผิดปกติ
Audit Logบันทึกว่าใครทำอะไร เมื่อใด — สำคัญเป็นพิเศษกับระบบการเงิน ERP และระบบที่ต้องตรวจสอบย้อนหลังได้ตามข้อกำหนดทางกฎหมาย/บัญชี

8Migration Patterns

ใช้ร่วมกับ Strangler Fig Pattern (หัวข้อ 1) ระหว่างย้ายระบบเก่าไปสู่ microservices

Anti-Corruption Layer
สร้าง adapter ระหว่าง model ของระบบเก่ากับระบบใหม่ — ไม่ให้โครงสร้างหรือศัพท์จาก legacy system ไหลเข้ามาปนกับ design ใหม่ทั้งหมด adapter ทำหน้าที่แปลความหมาย/รูปแบบข้อมูลระหว่างสองโลก เพื่อให้ระบบใหม่ออกแบบได้สะอาดโดยไม่ต้องแบกภาระทางเทคนิคของระบบเก่าไว้ในตัวเอง
Branch by Abstraction
สร้าง abstraction (เช่น interface) ครอบของเดิมไว้ก่อน แล้วค่อยเปลี่ยน implementation ด้านหลังทีละส่วนโดยที่ผู้เรียกใช้ไม่รู้ตัว — ทำให้เปลี่ยน implementation ได้แบบค่อยเป็นค่อยไปโดยไม่ต้องหยุดระบบเพื่อสลับทั้งหมดพร้อมกัน
Parallel Run
ให้ระบบเก่าและระบบใหม่ทำงานคู่กัน แล้วเปรียบเทียบผลก่อนสลับจริง — เหมาะกับ module ที่ผลลัพธ์ต้องแม่นยำมาก เช่น รายงานการเงิน หรือการคำนวณสิทธิ์ เพราะให้ความมั่นใจว่าระบบใหม่ให้ผลลัพธ์เหมือนระบบเก่าทุกกรณีก่อนตัดสินใจเลิกใช้ระบบเก่าจริง

9Anti-pattern ที่ควรสอนคู่กัน

บางครั้งนักศึกษาจำชื่อ pattern ได้ แต่ใช้ microservices โดยไม่เข้าใจต้นทุน จึงควรรู้ anti-pattern เหล่านี้ไว้ด้วย

Distributed Monolith
แยก deploy เป็นหลาย service แต่ทุก service ยังต้องเปลี่ยนและ deploy พร้อมกัน — ได้รับความซับซ้อนของ distributed system (network, partial failure, deployment coordination) มาเต็มๆ แต่ไม่ได้รับประโยชน์หลักของ microservices เลย (independent deployment) มักเกิดจากแบ่ง service ตามโครงสร้างเทคนิคผิด ไม่ใช่ตาม business capability ที่แท้จริง
Chatty Services
Service เรียกกันถี่มาก เช่นหนึ่ง request เกิด remote call หลายร้อยครั้ง — แต่ละ remote call มี latency และโอกาสล้มเหลวที่ local function call ไม่มี (ดู Remote Procedure Invocation หัวข้อ 2) ยิ่งเรียกถี่ยิ่งสะสม latency และความเสี่ยงมาก มักแก้ด้วยการออกแบบ API ให้หยาบขึ้น (coarser-grained) หรือใช้ Aggregator Pattern
Shared Database Everywhere
ทุก service เขียนทุกตารางได้ — จึงแยก ownership ของข้อมูลไม่ได้จริง ขัดกับหลักการพื้นฐานของ Database per Service (หัวข้อ 3) โดยตรง ทำให้เปลี่ยน schema ตารางใดก็เสี่ยงกระทบ service อื่นที่คาดไม่ถึง
Nano-services
แตก service เล็กเกินไป จนต้นทุน communication, deployment และ debugging สูงกว่างานที่ทำจริง — ขัดกับหลัก Single Responsibility per Service (หัวข้อ 1) ที่เน้น "หนึ่งเหตุผลหลักที่ต้องเปลี่ยน" ไม่ใช่ "หนึ่ง service ต่อหนึ่ง function"

Long Synchronous Chain

Client → A → B → C → D → E
เมื่อ request ต้องไล่เรียกแบบ synchronous ผ่านหลาย service ต่อกันเป็นสายยาว สองปัญหาสะสมไปพร้อมกัน: latency สะสมตลอดเส้นทาง (เวลารวม ≈ ผลรวมเวลาของทุก hop) และ availability รวมลดลงทวีคูณ
Client → A → B → C → D → E
คำนวณ Availability รวมของสาย 5 Service
ถ้าแต่ละ service พร้อมใช้งาน 99.9% (0.999) และต้องพึ่งพากันทั้งสาย (แบบอนุกรม):
Availability รวม = 0.999⁵ ≈ 0.995 = 99.5%
Downtime เพิ่มจาก ~0.1% เป็น ~0.5% ต่อ service ที่เพิ่มเข้ามาในสาย — ยิ่งสายยาว ยิ่งพร้อมใช้งานน้อยลง แม้แต่ละ service เดี่ยวๆ จะ "เชื่อถือได้" ก็ตาม
ข้อควรจำสำหรับสอบ
นี่คือเหตุผลเชิงคณิตศาสตร์ที่สนับสนุนการใช้ Asynchronous Messaging (หัวข้อ 2) แทน synchronous chain ยาวๆ เมื่อเป็นไปได้ — ตัดการพึ่งพาแบบ real-time ออกจากสายวิกฤต
Retry Storm
ทุก service retry พร้อมกันเมื่อ downstream service ล่ม ทำให้ service ที่กำลังฟื้นตัวถูกถล่มซ้ำด้วย request จำนวนมากพร้อมกัน — ยิ่งทำให้ฟื้นตัวยากขึ้นไปอีก (คล้าย congestion collapse ที่เรียนใน Congestion Control) ป้องกันด้วย Exponential Backoff + Jitter (หัวข้อ 4) ร่วมกับ Circuit Breaker (หัวข้อ 4)
Microservices by Default
ใช้ microservices ทั้งที่ระบบเล็ก ทีมเล็ก และยังไม่มีเหตุผลด้าน independent scaling หรือ independent deployment จริง — ต้นทุนของ microservices (network, partial failure, deployment coordination, observability) มีจริงเสมอไม่ว่าระบบจะเล็กแค่ไหน บางกรณี modular monolith (แบ่ง module ชัดเจนภายในโปรแกรมเดียว แต่ยัง deploy เป็นก้อนเดียว) เหมาะกว่า โดยเฉพาะช่วงเริ่มต้นที่ domain boundary ยังไม่นิ่งพอ
คำถามซ้อมสอบ
  1. อธิบายความแตกต่างระหว่าง Decompose by Business Capability กับ Decompose by Subdomain
  2. อธิบาย Saga Pattern พร้อมยกตัวอย่าง compensating action เมื่อขั้นตอนหนึ่งล้มเหลว และเปรียบเทียบ Choreography กับ Orchestration
  3. อธิบายว่า Event Sourcing ต่างจาก CRUD ทั่วไปอย่างไร และทำไมมักถูกใช้ร่วมกับ CQRS
  4. อธิบาย 3 สถานะของ Circuit Breaker และเชื่อมโยงว่ามันช่วยป้องกัน Retry Storm ได้อย่างไร
  5. คำนวณ availability รวมของ synchronous chain ที่มี 4 service ต่อกัน หากแต่ละ service พร้อมใช้งาน 99.5%
  6. อธิบายว่าทำไม Nano-services และ Distributed Monolith ถึงเป็น anti-pattern ทั้งที่หน้าตาต่างกัน (แตกเล็กเกินไป vs ไม่ได้แยกจริง)
  7. อธิบาย Transactional Outbox ว่าแก้ปัญหาอะไร และทำไม Idempotent Consumer ยังจำเป็นอยู่แม้มี Transactional Outbox แล้ว

10แล็ปออกแบบ Microservices จากโจทย์เดียวกัน

หลักคิดก่อนเริ่ม
Pattern ไม่ใช่ของประดับสถาปัตยกรรม แต่เป็นคำตอบที่มีต้นทุนต่อปัญหาบางชนิด เปรียบเหมือนเครื่องมือในกล่องช่าง: ค้อนดีมากเมื่อเจอตะปู แต่การถือค้อนแล้วมองทุกอย่างเป็นตะปูทำให้ระบบพัง แล็ปนี้จึงเริ่มจากแรงกดดันของระบบ แล้วค่อยเลือก pattern พร้อมระบุราคาที่ต้องจ่าย

โจทย์กลาง — ระบบสั่งอาหาร

ระบบมีลูกค้า ร้านอาหาร คนส่ง และการชำระเงิน ต้องรับ order กันของ ตัดเงิน แจ้งร้าน และตามสถานะ ให้เริ่มด้วย modular monolith ที่ deploy ก้อนเดียวก่อน แล้วใช้เหตุผลตัดสินว่าขอบเขตใดควรแยก ห้ามเริ่มจากวาดกล่อง service จำนวนมาก

การทดลองที่ 1 — หา Boundary จากภาษาของธุรกิจ

  1. เขียนเหตุการณ์สำคัญ เช่น OrderPlaced, PaymentAuthorized, RestaurantAccepted, DeliveryAssigned และ OrderCancelled
  2. จัดกลุ่มข้อมูลกับกฎที่ต้องเปลี่ยนพร้อมกัน ถ้าคำว่า “สถานะ” มีความหมายต่างกันใน order กับ delivery ให้แยก model
  3. เสนออย่างน้อยสองแบบ: modular monolith และ service boundary ที่เป็นไปได้ เปรียบเทียบ team ownership, deploy cadence, scaling และ transaction
  4. ระบุสิ่งที่ยังไม่ควรแยกเพราะ domain ยังไม่นิ่ง การรวมไว้ก่อนเป็นการตัดสินใจที่ดีได้ ไม่ใช่ความล้มเหลว
ขอบเขตข้อมูลที่เป็นเจ้าของเหตุผลที่เปลี่ยนควรแยกตอนนี้?
Orderorder, item, order statusกฎรับและยกเลิก orderให้ตอบพร้อมหลักฐาน
Paymentauthorization, refundผู้ให้บริการ/ข้อกำกับความเสี่ยงต่างจาก order
Deliveryrider, trip, locationการจับคู่และตำแหน่งสดรูปแบบโหลดต่างกัน

การทดลองที่ 2 — API Gateway ไม่ใช่ที่ทิ้ง Business Logic

สร้าง gateway ที่ route, authenticate และใส่ request ID จากนั้นทดลองทำ endpoint สำหรับ mobile ที่ต้องรวม order กับ delivery เปรียบเทียบ API Gateway ทั่วไปกับ BFF ที่ปรับตาม client โดยเฉพาะ

เส้นแบ่งที่ต้องอธิบาย
Gateway เหมือนประชาสัมพันธ์หน้าตึก: ตรวจบัตรและบอกทางได้ แต่ไม่ควรตัดสินอนุมัติคืนเงินแทนแผนกบัญชี ถ้า business rule ไหลเข้า gateway มาก ทุก service จะ deploy อิสระไม่ได้และ gateway กลายเป็น monolith ตัวใหม่

การทดลองที่ 3 — Sync หรือ Async

  1. ให้ order เรียก payment แบบ synchronous วัด latency และลองหยุด payment สังเกตผลต่อผู้ใช้
  2. เปลี่ยน notification เป็น event ผ่าน queue เพราะการส่งข้อความไม่ควรขวางการยืนยัน order
  3. กำหนด delivery guarantee, retry, DLQ และ idempotent consumer ทดลองส่ง event เดิมสองครั้ง
  4. ระบุงานที่ผู้ใช้ต้องรู้ผลทันที งานที่ยอมรับ eventual consistency และงานที่ไม่ควรทำหาย

การทดลองที่ 4 — Database per Service และข้อมูลที่อยาก JOIN

แยก ownership ของตาราง ห้าม order service อ่านตาราง payment โดยตรง สร้าง API composition สำหรับหน้ารายละเอียดหนึ่งครั้ง แล้วสร้าง read model ด้วย event อีกครั้ง เปรียบเทียบ freshness, latency, coupling และ failure mode

การทดลองที่ 5 — Saga และ Compensation

จำลอง workflow: สร้าง order → กันเงิน → ร้านรับ → จัดคนส่ง ทำให้แต่ละขั้นล้มเหลวทีละจุด แล้วเขียน compensating action เช่นคืนวงเงินหรือยกเลิกการจอง การชดเชยไม่ใช่การย้อนเวลา เพราะอาจมีผลที่ย้อนกลับไม่ได้ เช่นส่งข้อความถึงผู้ใช้แล้ว

ขั้นตอนพังสิ่งที่ทำสำเร็จแล้วการชดเชยกรณีที่ต้องให้คนตรวจ
payment ปฏิเสธสร้าง draft orderยกเลิก draftไม่จำเป็น
ร้านปฏิเสธหลังกันเงินกันวงเงินแล้วrelease authorizationrefund ไม่สำเร็จ
หาคนส่งไม่ได้ร้านเริ่มทำอาหารใช้ policy ธุรกิจอาจคืนเงิน/ชดเชย

ทำทั้ง choreography และ orchestration ในระดับ sequence diagram แบบแรกกระจายการตอบสนองตาม event และลดศูนย์กลาง แต่ตามเรื่องยากเมื่อ workflow โต แบบหลังเห็น state ชัดและควบคุมง่ายขึ้น แต่ orchestrator สำคัญและต้องทน failure

การทดลองที่ 6 — Transactional Outbox

  1. เขียน order ลงฐานข้อมูลแล้วให้ process ล้มก่อน publish event จะพบ dual-write problem
  2. สลับ publish ก่อน commit แล้วทำให้ database ล้ม จะได้ event ของข้อมูลที่ไม่มีจริง
  3. เขียน order กับ outbox row ใน transaction เดียว ให้ relay ส่ง event แล้วทำเครื่องหมายภายหลัง
  4. ทำให้ relay ส่งซ้ำก่อน mark-sent เพื่อพิสูจน์ว่า outbox ลดโอกาสหายแต่ไม่ได้กำจัด duplicate consumer จึงยังต้อง idempotent

การทดลองที่ 7 — Circuit Breaker, Bulkhead และ Rate Limit

สร้าง payment จำลองที่ช้า/พัง แล้ววัดระบบสี่แบบ: ไม่มีการป้องกัน, timeout+retry, circuit breaker, และ circuit breaker+bulkhead เปิดโหลดสองชนิดพร้อมกันคือ checkout กับดูประวัติ เพื่อดูว่าปัญหา checkout กิน worker ทั้งระบบหรือไม่

Patternคำถามที่ตอบต้นทุน
Circuit breakerเมื่อปลายทางพัง ควรหยุดเรียกชั่วคราวไหมมี state และ threshold ต้องปรับ
Bulkheadจะกั้นทรัพยากรไม่ให้ failure ลามอย่างไรทรัพยากรที่กั้นไว้อาจว่างขณะอีกส่วนแน่น
Rate limitใครส่งได้มากเท่าใดในช่วงเวลาต้องกำหนด identity และนโยบาย 429
Load sheddingเมื่อเกินกำลังจะปฏิเสธงานใดก่อนต้องรู้ลำดับความสำคัญของงาน

การทดลองที่ 8 — Observability เป็นส่วนหนึ่งของ Design

  1. ส่ง trace ID ผ่าน gateway, order, payment และ delivery แล้วสร้าง span ต่อ remote call
  2. เก็บ RED metrics: Rate, Errors, Duration ของแต่ละ endpoint และ resource saturation ที่เกี่ยวข้อง
  3. ทำให้หนึ่ง dependency ช้า หา critical path จาก trace แล้วเทียบกับการเดาจาก log ทีละไฟล์
  4. ตั้ง alert จากอาการที่ผู้ใช้ได้รับ เช่น error rate/latency ไม่ใช่จาก CPU อย่างเดียว และเขียน runbook ว่าคนรับ alert ตรวจอะไรเป็นลำดับแรก

การทดลองที่ 9 — Strangler Fig และการย้ายระบบทีละส่วน

วาง facade หน้า monolith route endpoint เดิมไปของเก่า และ route ความสามารถใหม่ไป service ใหม่ ทำ parallel run สำหรับการคำนวณค่าจัดส่ง เปรียบเทียบผลแต่ยังตอบผู้ใช้ด้วยระบบเดิม แล้วกำหนดเกณฑ์ว่าส่วนต่างเท่าใดจึงสลับจริงได้

Metaphor
Strangler Fig คล้ายสร้างสะพานใหม่ทีละช่วงข้างสะพานเดิม เราไม่รื้อสะพานเก่าทั้งหมดในคืนเดียว แต่ค่อยเปลี่ยนเส้นทาง ตรวจน้ำหนัก และมีทางย้อนกลับ จนแน่ใจว่าส่วนเก่าไม่ถูกใช้งานแล้วจึงรื้อ

การทดลองที่ 10 — ล่า Anti-pattern

ให้นักศึกษาตรวจแบบของตนด้วยคำถามต่อไปนี้ ถ้าตอบ “ใช่” ต้องเสนอการทดลองหรือข้อมูลที่ใช้ยืนยันว่าต้นทุนคุ้มค่า

Architecture Decision Record ที่ต้องส่ง

หัวข้อสิ่งที่ต้องเขียน
บริบทปัญหา ข้อจำกัด และคุณภาพระบบที่ต้องการ
ทางเลือกอย่างน้อยสองแบบ รวมทางเลือกที่ยังไม่แยก service
การตัดสินใจเลือกอะไร เพราะหลักฐานใด ไม่ใช่เพราะนิยม
ผลตามมาได้อะไร เสียอะไร failure ใหม่คืออะไร
เกณฑ์ทบทวนmetric หรือเหตุการณ์ใดทำให้กลับมาเปลี่ยนคำตอบ

11สรุปและขั้นตอนถัดไป

Microservices ที่ดีไม่ได้วัดจากจำนวนกล่องในแผนภาพ แต่วัดจากขอบเขตที่มีความหมาย ความสามารถในการเปลี่ยนอย่างอิสระ และการรับมือ failure โดยไม่ผลักความซับซ้อนไปให้ผู้ใช้ ขั้นถัดไปคือฝึกใช้เครื่องมือเครือข่ายเพื่อพิสูจน์ว่าอาการเกิดที่เครื่องเรา DNS เส้นทาง transport หรือบริการปลายทาง