โหมดมืด
บทที่ 8 — Microservices (สถาปัตยกรรมแบบบริการเล็ก ๆ หลายตัว) + Spring Cloud (ฉบับครบเครื่อง 2026)
หลังจากแอปของคุณโตขึ้น 1 monolith เริ่มไม่ไหว — บทนี้สอนตั้งแต่ "เมื่อไหร่ควรแยก" จนถึง "วิธีออกแบบระบบ distributed ระดับ production" โดยเรียงจากพื้นฐาน → ขั้นสูง
เป้าหมายของบทนี้: ทำให้คุณเข้าใจเหตุผล + รูปแบบ (patterns) + เครื่องมือ เพียงพอที่จะ (1) สื่อสารกับทีม architect ได้, (2) ตัดสินใจเลือกใช้/ไม่ใช้ microservices ได้, (3) เริ่มเขียน service ตัวจริงตามมาตรฐานปี 2026 ได้
🚧 โซนขั้นสูง — ข้ามได้
บทนี้ ไม่ได้อยู่ในเส้นทางพื้นฐาน — ถ้าเพิ่งจบบทที่ 5 ข้ามทั้งบทได้เลย Prerequisites ด้านล่างสำหรับผู้ที่อยากอ่านบทนี้จริง ๆ เท่านั้น
บทนี้เป็น บทอ้างอิงระดับ senior/architect (นักพัฒนาที่มีแอป Spring Boot ที่ใช้งานจริงอย่างน้อย 1 ตัวแล้ว และต้องการขยายระบบออกเป็นหลาย service) มือใหม่ ข้ามไปก่อนได้ทั้งบท — แอปส่วนใหญ่เริ่มจาก monolith (แอปเดียว) ก็เพียงพอและดีกว่าสำหรับทีมเล็ก microservices มาทีหลังเมื่อระบบโตจริง ๆ อ่านบทนี้เมื่อทีม/ระบบเริ่มใหญ่จนต้องแยกบริการ หรือเมื่ออยากเข้าใจศัพท์ที่ทีม architect ใช้คุยกัน
📋 Prerequisites — ควรรู้อะไรก่อน
อ่านบทนี้ให้ได้ประโยชน์เต็ม ควรเข้าใจหัวข้อต่อไปนี้แล้ว:
- ✅ Spring Boot พื้นฐาน — REST controller, dependency injection,
application.yml(บทที่ 1, บทที่ 2, บทที่ 3) - ✅ Database + JPA — เพราะจะคุยเรื่อง "DB ต่อ service" และ outbox table (บทที่ 2)
- ✅ Security + JWT — เพราะ Gateway จะ verify JWT (บทที่ 3)
- ✅ Production readiness — Actuator, metrics (บทที่ 5)
- 🟡 Async / Reactive — มีบางหัวข้อใช้
Monoแต่ไม่ลึก (บทที่ 10 มาทีหลังก็ได้) - 🟡 Messaging — เรื่อง Saga/Outbox จะแตะ Kafka (บทที่ 12)
ถ้ายังไม่แน่นบทไหน — กลับไปอ่านก่อน แล้วค่อยมาบทนี้
📖 Glossary — ศัพท์ที่ใช้บ่อย
แบ่งเป็น 3 ระดับ — ระดับ 1 ต้องรู้ก่อนอ่าน, ระดับ 2/3 เปิดดูตอนเจอในเนื้อหาก็ได้
ระดับ 1 — ศัพท์หลัก (ต้องรู้ก่อน)
| ศัพท์ | ความหมาย (สั้น) |
|---|---|
| Monolith (โมโนลิธ — แอปก้อนเดียว) | แอปเดียว, deploy เดียว, DB เดียว |
| Modular Monolith | Monolith ที่แบ่ง module ภายในชัดเจน (เตรียมพร้อมจะแยก) |
| Microservice (ไมโครเซอร์วิส — บริการเล็ก ๆ) | บริการเล็กๆ ที่ deploy แยก, มี DB ของตัวเอง, คุยผ่าน network |
| Service Discovery (เซอร์วิสดิสคัฟเวอรี — กลไกหา service) | กลไก "หา IP ของ service ที่ชื่อ X" — Eureka, K8s DNS, Consul |
| API Gateway (ประตู API) | ประตูเดียวที่ client คุยด้วย แล้ว route ไป service ภายใน |
| Idempotent (ไอเดมโพเทนต์ — ทำซ้ำได้ผลเดิม) | ทำซ้ำกี่ครั้งก็ผลเหมือนเดิม (สำคัญสำหรับ at-least-once messaging) |
| Eventual Consistency (อีเวนชวล คอนซิสเทนซี — ความสอดคล้องในที่สุด) | ข้อมูลใน service ต่างๆ ไม่ตรงกันชั่วคราว แต่จะตรงในที่สุด |
ระดับ 2 — ศัพท์เทคนิคขั้นกลาง (เจอใน Part 7-8)
| ศัพท์ | ความหมาย (สั้น) |
|---|---|
| Circuit Breaker (เซอร์กิตเบรกเกอร์ — ฟิวส์) | "ฟิวส์" — ถ้า downstream fail บ่อย → หยุดเรียกชั่วคราว |
| Bulkhead (บัลค์เฮด — ผนังกั้นห้อง) | จำกัด resource (thread) ที่ใช้เรียกแต่ละ service — กัน fail แพร่ |
| Backpressure (แบ็คเพรชเชอร์ — แรงต้านกลับ) | กลไกบอก producer "ช้าๆ หน่อย" เพื่อกัน consumer ตาย |
| Saga (ซากา — ลำดับ transaction) | sequence ของ local transaction ข้าม service + compensation (undo) ถ้า fail |
| Outbox (เอาท์บอกซ์ — กล่องส่งออก) | ตารางใน DB ที่เก็บ event รอ publish — กันกรณี "commit DB แล้ว publish ไม่สำเร็จ" |
| CAP Theorem (แคป ธีโอเร็ม — ทฤษฎีแคป) | ระบบ distributed เลือกได้แค่ 2 ใน |
| BFF (บีเอฟเอฟ — Backend For Frontend) | Gateway ที่ออกแบบเฉพาะสำหรับ frontend แต่ละประเภท เช่น web BFF ต่างจาก mobile BFF (รายละเอียด aggregate/shape data ดู §5.1) |
| Load Balancer (โหลดบาลานเซอร์ — ตัวกระจายภาระ) | กระจาย request ไปหลาย instance ของ service เดียวกัน กันไม่ให้ instance ใด instance หนึ่งรับภาระหนักเกิน |
| lb:// | scheme (รูปแบบ prefix ของ URL — เหมือนกับที่ http:// หรือ https:// ใช้นำหน้า URL) สำหรับ Spring Cloud LoadBalancer — เช่น lb://user-service |
ระดับ 3 — ศัพท์ขั้นสูง (เจอใน Part 11-13)
| ศัพท์ | ความหมาย (สั้น) |
|---|---|
| Service Mesh (เซอร์วิสเมช — ตาข่ายบริการ) | ชั้นโครงสร้างพื้นฐานที่ใช้ sidecar (container เสริมข้าง service) เป็น proxy ทำงานแทน — เช่น คุม traffic, เข้ารหัส mTLS, ส่ง telemetry — โดยที่ service code ไม่ต้องรู้ ตัวอย่าง: Istio, Linkerd |
| Sidecar (ไซด์คาร์ — รถพ่วงข้าง) | container เสริมที่รันคู่กับ service หลักใน Pod เดียวกัน (โดยทั่วไปเป็น proxy ของ mesh) |
| CDC (Change Data Capture) | อ่าน WAL/binlog ของ DB เป็น event stream (Debezium) — WAL (Write-Ahead Log) = ไฟล์ที่ DB จดทุกการเปลี่ยนแปลงไว้ก่อนเขียนจริง (Postgres), binlog = ของเทียบเท่าใน MySQL |
| mTLS | mutual TLS — ทั้ง client + server พิสูจน์ตัวตนกันด้วย certificate |
| Zero-trust (ซีโรทรัสต์ — ไม่ไว้ใจใคร) | ไม่ไว้ใจ network — auth ทุกๆ hop, แม้แต่ภายในคลัสเตอร์ |
| OpenTelemetry (OTel) | มาตรฐานเปิดสำหรับ trace + metrics + logs (รวบ standard เดิม OpenTracing/OpenCensus) |
💡 อ่านครั้งแรก ไม่ต้องจำหมด — กลับมาเปิดดูตอนเจอศัพท์ในเนื้อหา
Part 1: ทำไม Microservices (และทำไม "ยัง" ไม่ใช่)
1.1 Monolith vs Microservices
Monolith = แอปเดียว, deploy เดียว
Microservices = หลายแอป, deploy แยก, มี DB ของตัวเอง, คุยผ่าน network
| Monolith | Microservices | |
|---|---|---|
| Deploy | ครั้งเดียวจบ | หลายครั้ง, ต้อง CI/CD จริงจัง |
| Scale | scale ทั้งก้อน | scale เฉพาะ service ที่ต้องการ |
| Tech stack | 1 ภาษา | แต่ละ service เลือกได้ (Java + Go + Python) |
| Database | 1 DB | DB แยกต่อ service |
| Team | 1 team หรือหลายทีมในที่เดียว | หลาย team ทำงานอิสระจริง |
| Complexity | ต่ำ–กลาง | สูงมาก — network, partial failure, distributed tx |
| Latency | function call (ns) | network call (ms) |
| Debug | stack trace เดียว | ต้องใช้ distributed tracing |
| Testing | unit + integration | + contract + chaos + end-to-end |
1.2 ⚠️ เมื่อไหร่ควร / ไม่ควรใช้ Microservices
ไม่ใช่ทุกแอปต้องเป็น microservices — และส่วนใหญ่ "ยังไม่จำเป็น"
✅ ใช้เมื่อ:
- มี หลายทีม ทำงานคู่ขนาน (Conway's Law — โครงสร้างระบบ = โครงสร้างทีม)
- บาง service ต้อง scale ต่างจากที่อื่นมาก (เช่น search service โหลดมากกว่า user-service ราว 10 เท่า)
- บาง service ต้อง deploy บ่อย กว่าอันอื่น (ทุกชั่วโมง vs ทุกเดือน)
- ต้องใช้ tech stack ต่างกัน (Go สำหรับงาน CPU-bound, Python สำหรับ ML)
- ส่วนต่างๆ มี SLA / availability ต่างกัน (payment ห้ามล่ม, blog ล่มได้)
❌ อย่าเริ่ม project ใหม่ด้วย microservices:
- complexity สูง — network, partial failure, distributed transaction, eventual consistency
- การแบ่ง boundary ผิดตั้งแต่ต้นแก้ยากมาก (ต้องรู้ domain ก่อน)
- start จาก modular monolith → split ตอนจำเป็นจริง
- ทีมเล็ก (< 10 คน) แทบไม่ได้ประโยชน์จาก microservices เลย
"Microservices is not a goal. It's a consequence." — Sam Newman, Building Microservices(แปล: "Microservices ไม่ใช่เป้าหมาย แต่เป็นผลพลอยได้" — อย่าทำ microservices เพราะอยากทำ ให้ทำเพราะระบบมันโตจนต้องทำ) (Simon Brown เคยพูดในทำนองเดียวกันว่า ถ้า monolith ยังจัดโครงสร้างให้ดีไม่ได้ ก็ยากที่จะทำ microservices ให้ดีได้ — ฝึกจัดโครงสร้างให้ดีตั้งแต่ monolith ก่อน)
Anti-pattern ที่พบบ่อย: Distributed Monolith
ระบบที่แยกเป็น service แล้ว แต่ทุก service ต้อง deploy พร้อมกัน, share DB schema, หรือเปลี่ยน service A ต้องเปลี่ยน service B ด้วยเสมอ — ได้ปัญหาทั้ง 2 ฝั่ง: complexity ของ microservices + tight coupling ของ monolith
💡 เปรียบเทียบ: Distributed Monolith = แยกออกเป็นห้องคนละห้อง แต่ทุกห้องต้องเปิดประตูพร้อมกัน ปิดประตูพร้อมกัน ใช้ของในห้องเดียวกัน — ได้ปัญหาของ "แยกห้อง" (network, complexity) แต่ไม่ได้ประโยชน์ของ "ห้องอิสระ" (deploy แยก, scale แยก)
อาการ:
- "deploy service A ต้อง deploy service B ก่อน"
- "table นี้ใช้ใน 3 service"
- "เปลี่ยน DTO ต้องไปแก้ทุกที่" → คุณยังไม่ได้แยก service จริงๆ คุณแค่แยกไฟล์ออกจากกัน
1.3 12-Factor App — ปรัชญาพื้นฐาน
ก่อนสร้าง microservice — ทบทวน 12-Factor (12factor.net) ที่เป็นมาตรฐานของแอป cloud-native:
| # | Factor | ความหมาย |
|---|---|---|
| 1 | Codebase (ฐานโค้ด) | 1 repo ต่อ service, deploy ได้หลาย environment |
| 2 | Dependencies (ของที่ต้องใช้) | ประกาศชัดใน pom.xml/build.gradle, ไม่ใช้ system-wide |
| 3 | Config (การตั้งค่า) | เก็บใน environment variable, ไม่ฝังใน code |
| 4 | Backing services (บริการเสริม) | DB, cache, queue = resource ที่ attach ได้ผ่าน URL |
| 5 | Build, release, run (สร้าง/ปล่อย/รัน) | แยก 3 stage ชัด |
| 6 | Processes (กระบวนการ) | stateless (ไม่เก็บ state ไว้ใน process เอง) — state เก็บใน DB/cache แทน |
| 7 | Port binding (ผูกพอร์ต) | บริการตัวเองผ่าน port (ไม่พึ่ง container web server) |
| 8 | Concurrency (รันพร้อมกัน) | scale แนวนอน (หลาย process) |
| 9 | Disposability (พร้อมทิ้ง) | start/stop เร็ว, graceful shutdown |
| 10 | Dev/prod parity (dev = prod) | dev/staging/prod เหมือนกันมากที่สุด |
| 11 | Logs (บันทึก) | log เป็น stream (stdout), ไม่ใช่ไฟล์ — ให้ infra เก็บ |
| 12 | Admin processes (งาน admin) | one-off task (migration) = process แยก ใช้ code base เดียวกัน |
Spring Boot รองรับเกือบทุกข้อ "out of the box" (ใช้งานได้ทันทีโดยไม่ต้องตั้งค่าเอง — "แกะกล่องใช้ได้เลย") — แต่นักพัฒนาต้องไม่ละเมิดเอง (เช่น อย่าเก็บ config ใน code)
1.4 CAP Theorem + Eventual Consistency
CAP Theorem = ทฤษฎีพื้นฐานของระบบ distributed (กระจาย) — มี 3 สิ่ง ที่ขัดกัน เลือกได้แค่ 2:
text
Consistency
/ \
/ \
/ \
/ \
Availability───Partition tolerance- Consistency (ความสอดคล้อง) — ทุก node เห็น data เหมือนกันทันที
- Availability (พร้อมใช้งาน) — ทุก request ได้คำตอบ
- Partition tolerance (ทนต่อ network แตก) — ระบบทำงานต่อได้ แม้ network ขาด
P หลีกเลี่ยงไม่ได้ ในระบบจริง (network ขาดเป็นเรื่องปกติ) → ต้องเลือกระหว่าง CP (ความสอดคล้องมาก่อน — ถ้าไม่ sure ก็ reject ดีกว่าตอบผิด) หรือ AP (พร้อมใช้มาก่อน — ตอบไปก่อน ค่อย sync ทีหลัง)
💡 เปรียบเทียบ: เหมือนสาขาธนาคาร 2 สาขาที่สายแลนขาด — CP = "ปิดสาขา ห้ามถอนเงิน เพราะไม่รู้ยอดล่าสุด"; AP = "ให้ถอนได้ทั้ง 2 สาขา พอ network กลับมาค่อย reconcile (อาจเจอติดลบชั่วคราว)"
| CP | AP | |
|---|---|---|
| ทำงานเมื่อ network แตก | reject request | ตอบไปก่อน, sync ทีหลัง |
| ตัวอย่าง | RDBMS cluster, ZooKeeper, MongoDB (default write concern majority — CP-leaning แต่ tunable ได้) | Cassandra, DynamoDB, ระบบ event-driven ทั่วไป |
| Microservices ส่วนใหญ่ | ไม่ใช่ | ใช่ — เลือก availability + eventual consistency |
Eventual Consistency = "ข้อมูลใน service A กับ service B ไม่ตรงกันชั่วคราว แต่จะตรงในที่สุด"
ตัวอย่าง:
- User ชำระเงิน → Payment Service บันทึก "paid"
- ต้องแจ้ง Order Service ว่า paid แล้ว → ส่ง event
- ระหว่างที่ event ยังไม่ถึง — Order Service ยังเห็น "pending" → inconsistent ชั่วคราว
- หลังจาก consumer ประมวลผล event เสร็จ → Order = "paid" → consistent อีกครั้ง
→ ระบบของคุณต้อง ออกแบบให้ UX รองรับ inconsistency ชั่วคราวนี้ (เช่น แสดง "processing..." แทนที่จะ assume "paid ทันที")
Part 2: Spring Cloud + ทางเลือกในยุค 2026
2.1 Spring Cloud คืออะไร
Spring Cloud = ชุด library/abstraction สำหรับสร้าง microservices บน Spring Boot — แบ่งตามหน้าที่ได้ 3 กลุ่ม
A) Service Discovery + Routing — Part 3, 5
| Component | ทำอะไร | ยุค 2026 ยังใช้? |
|---|---|---|
| Eureka (Netflix) | Service Discovery | 🟠 legacy (ยังใช้งานได้แต่ไม่แนะนำสำหรับระบบใหม่) — ใช้กับระบบเก่าที่มีอยู่; สำหรับ microservices ใหม่พิจารณา Consul/Nacos หรือ K8s DNS |
| Spring Cloud Gateway | API Gateway (reactive) | ✅ ใช้แพร่หลาย |
| Spring Cloud Gateway MVC (ใหม่ 2024+) | API Gateway (servlet) | ✅ ทางเลือกใหม่ สำหรับทีมที่ไม่อยาก reactive |
B) Communication + Resilience — Part 4, 7
| Component | ทำอะไร | ยุค 2026 ยังใช้? |
|---|---|---|
| Spring Cloud LoadBalancer | Client-side LB (แทน Ribbon ที่ deprecated แล้ว) | ✅ |
| OpenFeign | HTTP client declarative | 🟡 ยังใช้งานได้ดีกับ Spring Boot 3.x แต่โปรเจกต์ใหม่แนะนำ HTTP Interface (@HttpExchange) แทน |
| Resilience4j | Circuit Breaker, Retry, Bulkhead, Rate Limiter | ✅ |
| Spring Cloud Stream | Event-driven abstraction (Kafka/Rabbit) | 🟡 ใช้ Kafka SDK ตรงๆ ได้เช่นกัน |
C) Operations: Config + Secret + Observability + Contract — Part 6, 10, 11
| Component | ทำอะไร | ยุค 2026 ยังใช้? |
|---|---|---|
| Spring Cloud Config | Centralized config | 🟡 K8s ConfigMap/Secret + Vault แทนได้ |
| Spring Cloud Vault | Secret management | ✅ |
| Micrometer + OpenTelemetry | Metrics + Tracing | ✅ standard |
| Spring Cloud Kubernetes | Bridge ไป K8s primitive | ✅ ในระบบ K8s |
| Spring Cloud Contract | Consumer-driven contract testing | ✅ |
💡 ข้อสำคัญ: ปี 2026 หลายอย่างของ Spring Cloud "ทับซ้อน" กับ K8s + Service Mesh — เลือกใช้ตามบริบทของ deployment ไม่ใช่ใช้ทุกตัว
2.2 โปรเจกต์ตัวอย่าง
ตลอดบทนี้จะอ้างถึงระบบตัวอย่างเดียวกัน — e-commerce ที่แตกเป็นหลาย service (user/order/payment) มี gateway, Eureka, config server, tracing ครบ แผนภาพนี้แสดงภาพรวมของระบบตัวอย่าง แต่ละชิ้นส่วนที่เรียนต่อ ๆ ไปจะวางอยู่ตรงไหนในภาพนี้:
ทุก service register → Eureka (8761) · ทุก service fetch config → Config Server (8888)
Part 3: Service Discovery
3.1 ปัญหา: hardcode URL
java
String userServiceUrl = "http://localhost:8081"; // ❌ hardcode
restClient.get().uri(userServiceUrl + "/users/1");ปัญหาที่ตามมา:
- Scale เป็น 5 instance → ใช้ instance ไหน?
- Deploy ใหม่ → IP เปลี่ยน?
- Instance ตาย → request ไป IP ตายนั้นต่อไหม?
ทางแก้ — Service Discovery: มี registry กลาง ที่ทุก service register ตอน start + client query หา instance ที่ live
มี 2 รูปแบบ:
| Client-side discovery (Eureka) | Server-side discovery (K8s, AWS ALB) | |
|---|---|---|
| ใครเลือก instance | client | proxy/LB |
| Resolve ที่ไหน | application code | infrastructure |
| ตัวอย่าง | Eureka + Spring Cloud LoadBalancer | K8s Service + kube-proxy, AWS ALB |
3.2 Eureka Server — Service Registry
🟠 อ่านก่อน — สถานะ Eureka ปี 2026: Eureka ยังใช้ได้แต่ไม่พัฒนาต่อแล้ว (maintenance mode = แค่ patch ความปลอดภัย ไม่มี feature ใหม่; Eureka 2.x ถูกยกเลิกก่อน release) เนื้อหาข้างล่างคงไว้เพราะยังมีระบบเก่าใช้อยู่จำนวนมาก
สำหรับ microservices ใหม่ปี 2026 — ใช้ตัวอื่น:
- บน Kubernetes → ใช้ K8s DNS / Service built-in (ดู §3.5 — เป็นทางเลือกหลัก)
- On-prem ไม่มี K8s → Consul (HashiCorp) หรือ Nacos (Alibaba) ทั้งคู่ยังพัฒนาต่อ + มี health check / config center ในตัว
Tool สถานะ จุดเด่น K8s DNS built-in ไม่ต้องลง registry แยก ถ้าอยู่บน K8s แล้ว (ทางเลือกหลักปี 2026) Consul (HashiCorp) active health check + KV (key-value store) + multi-DC (หลาย data center) + ACL (Access Control List — ควบคุมสิทธิ์การเข้าถึง) ในตัว Nacos (Alibaba) active service discovery + config center รวมตัวเดียว Eureka 1.x maintenance — ปลอดภัยใช้ต่อแต่ไม่มี feature ใหม่ ecosystem Spring Cloud Netflix ครบ — เหมาะกับการ migrate ระบบเก่า etcd active, K8s ใช้ภายใน low-level — มัก wrap ผ่าน operator เนื้อหา Eureka ข้างล่างยังใช้ได้สำหรับ อ่านเข้าใจระบบเก่า หรือ on-prem ที่ไม่ migrate — แต่ถ้าเริ่มโปรเจกต์ใหม่ปี 2026 กระโดดไปดู §3.5 (K8s DNS) ก่อน
Eureka คือ service registry ฝั่ง client-side discovery — สร้างง่ายมาก แค่ @EnableEurekaServer บน Spring Boot app ตัวหนึ่ง ก็ได้ registry กลางที่ service อื่นมา register และ query หากันได้ ตั้ง register-with-eureka: false เพราะ server เองไม่ต้อง register ตัวเอง:
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>java
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApp {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApp.class, args);
}
}yaml
# application.yml
server:
port: 8761
eureka:
client:
register-with-eureka: false # server ตัวเองไม่ register
fetch-registry: falseRun → เปิด http://localhost:8761 → เห็น dashboard
3.3 Eureka Client
ฝั่ง service ที่จะ register เข้า Eureka — เพิ่ม eureka-client dependency แล้วชี้ไปที่ Eureka server พอ start service จะ register ตัวเองอัตโนมัติ (ชื่อ + IP + port) และดึงรายชื่อ service อื่นมา cache เพื่อเรียกด้วยชื่อแทน hardcode IP:
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>yaml
spring:
application:
name: user-service # ⭐ ชื่อนี้คือ "service ID"
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true # register ด้วย IP แทน hostname (ดีในงาน container)Start → service จะ register ตัวเอง ส่ง heartbeat ทุก 30 วินาที
ใน Eureka dashboard:
text
USER-SERVICE UP 192.168.1.10:8081
ORDER-SERVICE UP 192.168.1.11:8082
PAYMENT-SERVICE UP 192.168.1.12:80833.4 เรียก service ผ่านชื่อ (load-balanced)
Spring Cloud LoadBalancer (แทน Ribbon ที่ deprecated แล้ว) จะ resolve ชื่อ service เป็น instance + load-balance ให้
java
@Bean
@LoadBalanced
public RestClient.Builder restClientBuilder() {
return RestClient.builder();
}⚠️
@LoadBalancedต้องใส่บน Builder (เช่นRestClient.Builder/WebClient.Builder) ไม่ใช่บน RestClient ที่ build แล้ว — ถ้าใส่ผิดที่ load balancing จะเงียบ ไม่ทำงาน ไม่ throw error ·@LoadBalancedบนWebClient.Builderใช้งานได้เช่นกันหาก inject ผ่าน@Autowired WebClient.Builderปกติ
java
@Service
@RequiredArgsConstructor
public class OrderService {
private final RestClient.Builder builder;
public User getUser(Long id) {
return builder.build().get()
.uri("http://user-service/users/{id}", id) // ⭐ ชื่อ service, ไม่ใช่ URL
.retrieve()
.body(User.class);
}
}Spring Cloud LoadBalancer ทำงาน:
- resolve
user-service→ ดูใน Eureka → ได้ list ของ instance live - เลือก 1 instance (default = round-robin; เปลี่ยนเป็น weighted/random/health-aware ได้)
- ส่ง request
3.5 ⚠️ Service Discovery ในยุค Kubernetes (2026)
ถ้า deploy บน Kubernetes — คุณไม่ต้องใช้ Eureka เพราะ K8s มี Service Discovery built-in ผ่าน CoreDNS
yaml
# k8s Service
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector:
app: user-service
ports:
- port: 8081ใน code → เรียกผ่าน DNS ตรงๆ:
java
RestClient.create("http://user-service:8081") // K8s DNS resolve ให้
.get().uri("/users/{id}", id)
.retrieve().body(User.class);💡
RestClient.create(String)ต้องการ Spring 6.1+ / Spring Boot 3.2+ · ในเวอร์ชันเก่ากว่านี้ ใช้ builder pattern แทน เช่นRestClient.builder().baseUrl("http://user-service:8081").build()🆕 ปี 2026 — สำหรับ blocking call ใช้
RestClient(Spring 6.1+) เป็นทางเลือกหลัก ไม่ใช่RestTemplate(legacy) และไม่ใช่WebClient(เหมาะกับ reactive/non-blocking)RestClientมี fluent API คล้ายWebClientแต่ blocking ตรง ๆ — ไม่ต้องเรียน Reactor
K8s จัดการให้:
- Service Discovery → CoreDNS resolve
user-service.<namespace>.svc.cluster.local→ ClusterIP - Load Balancing → kube-proxy round-robin ไปแต่ละ Pod
- Health Check → liveness/readiness probe ลบ Pod ที่ unhealthy ออกจาก endpoint
- Self-healing → Deployment สร้าง Pod ใหม่อัตโนมัติ
| Eureka | K8s DNS | Service Mesh (Istio) | |
|---|---|---|---|
| ทำงานที่ไหน | application | infrastructure (DNS) | infrastructure (sidecar proxy) |
| ต้อง code อะไร | แค่เพิ่ม eureka-client dependency + config defaultZone (auto-detected via classpath — @EnableEurekaClient deprecated แล้ว ไม่ต้องใส่) + lb:// | แค่เรียก DNS name | แค่เรียก DNS name |
| ต้อง host server เอง | ใช่ (Eureka) | ไม่ — K8s control plane | ไม่ — control plane ของ mesh |
| Multi-language | ต้องมี Eureka client ต่อภาษา | ใช้ทุกภาษา (DNS standard) | ใช้ทุกภาษา + ได้ feature เพิ่ม (mTLS, traffic split) |
| ตำแหน่งปี 2026 | non-K8s deployment / on-prem | mainstream | ทีมที่ต้องการ traffic mgmt + zero-trust |
สรุปการเลือก:
- เริ่มเรียน / โปรเจกต์ on-prem ไม่มี K8s → Eureka
- Deploy บน K8s → K8s DNS (ไม่ต้อง Eureka)
- ต้องการ mTLS, canary, retry, telemetry แบบไม่แตะ code → Service Mesh (ดู Part 12)
💡 Spring Cloud Kubernetes = bridge layer ถ้าอยากใช้ syntax
@LoadBalanced/lb://กับ K8s — แต่ส่วนใหญ่ใช้ DNS ตรงๆ ง่ายกว่า
Part 4: HTTP Client — Feign vs HTTP Interface
4.1 ปัญหา: เขียน HTTP client เยอะ
การเรียก service อื่นด้วย RestClient ต้องเขียน URL + retrieve + map type ซ้ำทุก endpoint น่าเบื่อและพลาดง่าย "Declarative client" (client แบบประกาศ) คือแนวทางที่บอกแค่ว่า endpoint หน้าตาอย่างไร ไม่ต้องเขียน logic เรียก HTTP เอง แก้ปัญหาข้างต้นโดยให้เราประกาศ interface แล้ว framework generate implementation ให้:
java
return restClient.get().uri("http://user-service/users/{id}", id).retrieve().body(User.class);แต่ละ endpoint ต้องเขียนซ้ำ — declarative client มาช่วย
4.2 OpenFeign — ทางเลือกดั้งเดิม
OpenFeign เป็น declarative client ที่ใช้มานาน — ประกาศ interface พร้อม annotation (@GetMapping) แล้ว Feign สร้าง HTTP client ให้ บวกกับ integrate Eureka/load balancer ในตัว (ปัจจุบัน Spring มี HTTP Interface เป็นทางเลือกใหม่ที่เบากว่า ดูหัวข้อถัดไป):
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>java
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApp { ... }java
@FeignClient(name = "user-service") // ⭐ ชื่อ service ใน Eureka/K8s
public interface UserServiceClient {
@GetMapping("/users/{id}")
User getById(@PathVariable Long id);
@GetMapping("/users")
List<User> getAll(@RequestParam(required = false) String name);
@PostMapping("/users")
User create(@RequestBody CreateUserRequest req);
}ใช้:
java
@Service
@RequiredArgsConstructor
public class OrderService {
private final UserServiceClient userClient;
public Order createOrder(Long userId, List<Item> items) {
User user = userClient.getById(userId); // HTTP call ภายใต้
...
}
}Feign จัดการ: resolve URL จาก service registry, load balance, serialize/deserialize JSON, timeout/retry
4.3 ✨ HTTP Interface — ทางเลือกใหม่ใน Spring 6 / Boot 3+
Spring Framework 6 มี HTTP Interface built-in (ไม่ต้องพึ่ง Spring Cloud) — กำลังเป็นทางเลือกหลักแทน Feign
java
public interface UserServiceClient {
@GetExchange("/users/{id}")
User getById(@PathVariable Long id);
@PostExchange("/users")
User create(@RequestBody CreateUserRequest req);
}java
@Configuration
public class HttpClientConfig {
@Bean
public UserServiceClient userServiceClient(RestClient.Builder builder) {
RestClient restClient = builder
.baseUrl("http://user-service") // หรือใช้ @LoadBalanced builder
.build();
var adapter = RestClientAdapter.create(restClient);
var factory = HttpServiceProxyFactory.builderFor(adapter).build();
return factory.createClient(UserServiceClient.class);
}
}⚠️ เลือก adapter ตาม stack (ชุด library ที่ใช้ทำงานด้วยกัน): servlet stack (
spring-boot-starter-web) →RestClientAdapter(แบบบน) · reactive stack (spring-boot-starter-webflux) →WebClientAdapterใส่ผิดจะ inject ไม่ได้ (Spring ไม่สามารถสร้าง bean และเชื่อมให้อัตโนมัติได้)
Feign vs HTTP Interface
| OpenFeign | Spring HTTP Interface | |
|---|---|---|
| Maintained by | OpenFeign + Spring Cloud team | Spring core team |
| Annotation | @FeignClient, @GetMapping | @GetExchange, @PostExchange |
| Default underlying client | OkHttp / Apache HttpClient (library สำหรับส่ง HTTP request ของ Java) | RestClient / WebClient |
| Reactive | partial (need extension) | ✅ ตรงๆ ผ่าน WebClient |
| ต้องการ Spring Cloud | ✅ | ❌ (อยู่ใน Spring core) |
| Decoder/encoder | custom-ize ผ่าน config | ใช้ของ Spring (Jackson, ฯลฯ) ตรงๆ |
| โครงการใหม่ปี 2026 | ใช้ได้ | แนะนำใช้ |
ทำไมแนะนำ HTTP Interface: เป็นส่วนหนึ่งของ Spring core, integrate กับ observability stack, type-safe กว่าเดิม, รองรับทั้ง blocking + reactive ตรงๆ ผ่าน builder เดียวกัน
Part 5: API Gateway
5.1 ทำไมต้องมี Gateway
Client (web/mobile) ไม่ควรรู้ ว่ามีกี่ service อยู่ที่ไหน → Gateway = entry point เดียว
Gateway ทำหน้าที่:
- Routing —
/api/users/*→ user-service,/api/orders/*→ order-service - Auth — verify JWT ครั้งเดียว ส่งต่อ
X-User-Idheader - Rate limit — กัน abuse
- CORS — handle preflight
- Request/Response transformation — strip prefix, add header
- Aggregation (BFF pattern) — merge 3 service เป็น 1 response
- Logging + tracing — entry point ดี ที่จะเริ่ม trace
⚡ Backend For Frontend (BFF)
Mobile กับ web ต้องการ data ต่างกัน → ทำ Gateway แยกตาม client:
แต่ละ BFF aggregation/shape data ตาม UI ของ client นั้นๆ — ลด round trip, ลด over-fetching
5.2 Spring Cloud Gateway (Reactive) — ทางเลือกหลัก
Spring Cloud Gateway เป็น API gateway มาตรฐานของ Spring (สร้างบน WebFlux/reactive) — กำหนด route (predicate → service), แปะ filter (auth, rate limit, circuit breaker) ที่ขอบระบบจุดเดียว ผูกกับ Eureka ด้วย lb:// เพื่อ load balance ไป instance อัตโนมัติ:
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>yaml
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
discovery:
locator:
enabled: true # auto-route ตามชื่อ service ใน Eureka
routes:
- id: user-route
uri: lb://user-service # lb:// = load-balanced
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1 # ตัด path segment แรกออก: /api/users/1 → /users/1 (StripPrefix=1 หมายถึงตัด 1 segment)
- AddRequestHeader=X-Source, gateway
- id: order-route
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eurekaตอนนี้:
GET /api/users/1→ forward →user-service:/users/1GET /api/orders/5→ forward →order-service:/orders/5
5.3 ✨ Spring Cloud Gateway Server MVC (ใหม่ 2024+)
ตั้งแต่ Spring Cloud 2024.0 (Leyton) มี Gateway MVC — เวอร์ชัน servlet-based สำหรับทีมที่ไม่อยากใช้ Reactor
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-mvc</artifactId>
</dependency>java
// import ที่ต้องใช้:
// import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
// import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
// import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.stripPrefix;
// import org.springframework.web.servlet.function.RouterFunction;
// import org.springframework.web.servlet.function.ServerResponse;
@Configuration
public class GatewayConfig {
@Bean
public RouterFunction<ServerResponse> userRoute() {
return route("user-route")
.GET("/api/users/**", http("lb://user-service"))
.filter(stripPrefix(1)) // ใช้ .filter() ไม่ใช่ .before()
.build();
}
}| Gateway (Reactive) | Gateway MVC | |
|---|---|---|
| Stack | WebFlux + Netty | Servlet + Tomcat |
| Throughput สูงสุด | สูงกว่า (non-blocking) | กลาง |
| ใช้เมื่อ | high concurrency, streaming | ทีมยังไม่ถนัด reactive |
| Debug | reactive stack ยาก | stack เดียวกับ MVC ปกติ |
💡 ทั้งสองตัวมี predicate/filter เหมือนกัน — เปลี่ยนทีหลังได้
5.4 Predicate และ Filter
Predicate = "request ตรงเงื่อนไขไหม?"
yaml
predicates:
- Path=/api/users/**
- Method=GET
- Header=Authorization, Bearer.*
- Query=lang, th
- After=2026-01-01T00:00:00+07:00[Asia/Bangkok]
- Cookie=session, .*
- Host=**.example.comFilter = "ทำอะไรก่อน/หลัง forward"
yaml
filters:
- StripPrefix=1
- AddRequestHeader=X-API-Version, v2
- AddResponseHeader=X-Service, gateway
- RewritePath=/api/(?<segment>.*), /$\{segment}
- CircuitBreaker=userServiceCB
- RequestRateLimiter=...
- RemoveRequestHeader=Cookie
- Retry=35.5 Custom Filter — JWT Auth
java
@Component
@RequiredArgsConstructor // inject JwtService ผ่าน constructor (JwtService มาจากบทที่ 3 — copy มาไว้ใน gateway project พร้อม secret เดียวกัน)
public class JwtAuthFilter implements GlobalFilter, Ordered {
private final JwtService jwtService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// หยุดทันที ไม่ส่ง request ต่อไปยัง service ข้างหลัง
return exchange.getResponse().setComplete();
}
String token = authHeader.substring(7);
try {
Claims claims = jwtService.parse(token);
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-User-Id", claims.getSubject())
.header("X-User-Role", claims.get("role", String.class))
.build();
return chain.filter(exchange.mutate().request(request).build());
} catch (io.jsonwebtoken.JwtException e) {
// JwtException ครอบ ExpiredJwtException, MalformedJwtException, SignatureException — token ไม่ valid
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
} catch (Exception e) {
// bug ใน code เอง (NullPointerException ฯลฯ) — log และ return 500 ไม่ใช่ 401
exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
return exchange.getResponse().setComplete();
}
}
@Override
public int getOrder() { return -1; } // ทำก่อน filter อื่น
}💡
Mono<Void>= ผลลัพธ์แบบ reactive ที่ไม่มีค่าส่งกลับ — ดูบทที่ 10 ถ้ายังไม่คุ้น ตรงนี้แค่หมายความว่า "filter นี้ทำงานเสร็จเมื่อ chain (ลำดับ filter ทั้งหมด) ทำเสร็จ"
→ Service หลังไม่ต้อง verify JWT ซ้ำ — แค่อ่าน X-User-Id จาก header
Part 6: Configuration + Secrets
6.1 ปัญหา: config กระจาย
10 service × application.yml ต่อ environment (dev/staging/prod)
จะเปลี่ยน DB password → แก้ 30 ไฟล์ → restart ทุก service
ทางออก:
- Spring Cloud Config — config ใน Git repo
- HashiCorp Vault / Spring Cloud Vault — secret encrypted
- K8s ConfigMap + Secret — สำหรับ K8s
- AWS Parameter Store / Secrets Manager — สำหรับ AWS
6.2 Spring Cloud Config Server
Spring Cloud Config Server แก้ปัญหา config กระจายโดยดึงจาก Git repo กลาง — @EnableConfigServer + ชี้ไป Git แล้ว service ทุกตัวดึง config ตอน start ได้ประโยชน์เรื่อง version control / audit / rollback ของ config ฟรีจาก Git:
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>java
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApp { ... }yaml
server:
port: 8888
spring:
cloud:
config:
server:
git:
uri: https://github.com/myorg/config-repo
default-label: main
search-paths: '{application}' # subfolder ตามชื่อ serviceGit repo:
text
config-repo/
├── application.yml ← shared (ทุก service)
├── user-service.yml ← เฉพาะ user-service
├── user-service-dev.yml ← user-service profile=dev
├── user-service-prod.yml
└── order-service.yml6.3 Setup Client
Spring Boot 2.4+ ใช้ spring.config.import (ไม่ต้อง bootstrap.yml แล้ว):
yaml
# application.yml
spring:
application:
name: user-service
config:
import: optional:configserver:http://localhost:8888
profiles:
active: dev⚠️ ระวัง: บางทีมยังใช้
bootstrap.ymlแบบเดิม — ทั้งสองวิธีใช้งานได้ แต่spring.config.importเป็นทางการของ 2.4+
6.4 Refresh config โดยไม่ restart
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>yaml
management:
endpoints:
web:
exposure:
include: refresh,health,infoใส่ @RefreshScope บน bean ที่ใช้ config:
java
@Component
@RefreshScope
@ConfigurationProperties("feature")
public class FeatureFlags {
private boolean newCheckout;
// getter/setter
}แก้ config ใน Git → push → POST /actuator/refresh → config ใหม่ (โดยไม่ restart)
💡 ใน production ใช้ Spring Cloud Bus + RabbitMQ/Kafka เพื่อ broadcast (ส่งข้อมูลหาทุก instance พร้อมกัน)
refreshไปทุก instance — ไม่ต้อง POST ทีละ instance⚠️ ต้องปิดกั้น
/actuator/refreshไม่ให้ public เข้าถึงได้ เช่น เพิ่ม Spring Security rule หรือตั้งmanagement.server.portแยก — ไม่งั้นใครก็ trigger refresh ได้
6.5 ✨ Spring Cloud Vault — Secret Management
DB password, API key ไม่ควรอยู่ใน Git → ใช้ HashiCorp Vault
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-vault-config</artifactId>
</dependency>yaml
spring:
config:
import: vault://
cloud:
vault:
uri: https://vault.internal:8200
authentication: KUBERNETES # หรือ TOKEN, APPROLE
kubernetes:
role: user-service
kv:
backend: secret
application-name: user-serviceSecret ที่ Vault ตำแหน่ง secret/user-service จะถูก inject เป็น property → ${db.password} ใช้ได้เลย
Vault ยังทำ dynamic secret ได้: สร้าง DB credential ใหม่ทุกครั้งที่ service start (rotate auto)
6.6 Config ในยุค Kubernetes
K8s มี primitive ของตัวเอง:
yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: user-service-config
data:
application.yml: |
feature:
newCheckout: true
---
apiVersion: v1
kind: Secret
metadata:
name: user-service-secrets
type: Opaque
stringData:
DB_PASSWORD: <your-db-password> # ⚠️ K8s Secret เป็นแค่ base64 ไม่ใช่ encryption — ใน production ควรใช้ Vault หรือ Sealed Secrets แทนMount เป็น file / env variable → Spring Boot อ่านได้ตรง
| Spring Cloud Config | K8s ConfigMap + Secret | Vault | |
|---|---|---|---|
| Versioning | Git history | ไม่มี (ต้อง GitOps) | Built-in |
| Secret encryption | ผ่าน Jasypt / Vault | base64 (การเข้ารหัสข้อมูลให้เป็นตัวอักษร — ไม่ใช่การเข้ารหัสเพื่อความปลอดภัย ใครก็ decode กลับได้ง่าย ๆ) | ✅ encrypt at rest |
| Hot reload | @RefreshScope + Bus | restart pod หรือใช้ reloader sidecar | ✅ lease + auto-renew |
| Dynamic secret | ❌ | ❌ | ✅ |
→ ปี 2026 หลายทีม mix: K8s ConfigMap (non-secret) + Vault (secret)
Part 7: Resilience — Fault Tolerance
7.1 ปัญหา: Cascade Failure
text
User → Order Service → Payment Service (ล่ม!)Order รอ Payment 30 วินาที → request queue บวม → Order หมด thread → Order ก็ล่มตาม → cascade
ทางแก้ — มี 5 pattern หลัก (ทำงานด้วยกัน):
| Pattern | ทำอะไร | ตัวอย่าง |
|---|---|---|
| Circuit Breaker | "ฟิวส์" — fail 50% → หยุดเรียก fail fast (ล้มเหลวทันที — ไม่รอ timeout) | Resilience4j CircuitBreaker |
| Retry | ลองอีกครั้ง (สำหรับ transient error) | Resilience4j Retry |
| Time Limiter | จำกัดเวลารอ → fail แทนรอนาน | Resilience4j TimeLimiter |
| Bulkhead | แยก resource pool ต่อ downstream → fail ไม่แพร่ | Resilience4j Bulkhead |
| Rate Limiter | จำกัด req/sec ที่ส่งออก → ไม่ถล่ม downstream | Resilience4j RateLimiter |
7.2 Setup Resilience4j
xml
<!-- สำหรับ servlet stack (Spring MVC / Feign / blocking) ใช้ตัวนี้ -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
<!-- มี resilience4j-spring-boot3 autoconfiguration มาพร้อมแล้ว ไม่ต้องเพิ่มแยก -->
</dependency>
<!-- ถ้าใช้ reactive stack (WebFlux) ให้ใช้ spring-cloud-starter-circuitbreaker-reactor-resilience4j แทน -->yaml
resilience4j:
circuitbreaker:
instances:
paymentService:
slidingWindowSize: 10 # ดู 10 call ล่าสุด
minimumNumberOfCalls: 5 # ต้องมีอย่างน้อย 5 ก่อนคำนวณ
failureRateThreshold: 50 # 50% fail → OPEN
waitDurationInOpenState: 30s # รอ 30s ก่อนเข้าสถานะ HALF_OPEN
permittedNumberOfCallsInHalfOpenState: 3
slowCallDurationThreshold: 2s # call > 2s นับเป็น "slow"
slowCallRateThreshold: 50 # slow 50% → OPEN
retry:
instances:
paymentService:
maxAttempts: 3
waitDuration: 500ms
enableExponentialBackoff: true # ต้องตั้งค่านี้ก่อน multiplier ถึงจะทำงาน
exponentialBackoffMultiplier: 2 # 500ms → 1s → 2s
# ⚠️ ระวัง — Retry สำหรับ POST/PUT ต้องคู่กับ idempotency เสมอ
# ไม่งั้น downstream อาจ "ชาร์จเงินซ้ำ" ตอน retry — ดู §8.4 (Idempotency-Key)
timelimiter:
instances:
paymentService:
timeoutDuration: 2s
cancelRunningFuture: true
bulkhead:
instances:
paymentService:
maxConcurrentCalls: 25 # max 25 call พร้อมกัน
ratelimiter:
instances:
paymentService:
limitForPeriod: 100 # 100 call
limitRefreshPeriod: 1s # ต่อ 1 วินาที
timeoutDuration: 0 # ถ้าเกิน → throw ทันทีค่า config แต่ละตัวหมายอะไร
- slidingWindowSize: ขนาดของ "หน้าต่าง" ที่ใช้คำนวณ fail rate (count-based)
- failureRateThreshold: % ของ fail ที่ทำให้ CB เปลี่ยนเป็น OPEN
- waitDurationInOpenState: หลัง OPEN รอกี่นาน ก่อนลอง (HALF_OPEN)
- permittedNumberOfCallsInHalfOpenState: ระหว่าง HALF_OPEN ยอม call กี่ครั้ง ก่อนตัดสินใจ
- slowCallDurationThreshold: call ที่ใช้เวลาเกินนี้ → นับเป็น slow (ไม่ต้อง error)
7.3 ใช้ Annotation
java
@Service
@RequiredArgsConstructor
@Slf4j
public class PaymentClient {
private final PaymentServiceClient feignClient;
private final Executor paymentExecutor; // inject จาก @Bean ข้างล่าง — ขนาด pool ควรตรงกับ bulkhead maxConcurrentCalls
@CircuitBreaker(name = "paymentService", fallbackMethod = "fallback")
@Retry(name = "paymentService")
@TimeLimiter(name = "paymentService")
@Bulkhead(name = "paymentService", type = Bulkhead.Type.SEMAPHORE)
public CompletableFuture<Payment> charge(BigDecimal amount) {
return CompletableFuture.supplyAsync(() -> feignClient.charge(amount), paymentExecutor);
}
public CompletableFuture<Payment> fallback(BigDecimal amount, Throwable t) {
log.warn("Payment service down — using fallback. cause={}", t.toString());
return CompletableFuture.completedFuture(Payment.pending(amount));
}
}
// ⭐ ต้องมี bean นี้จริง ไม่ใช่แค่ comment — ถ้าไม่มี Spring หา bean type Executor ไม่เจอ
// app จะ start ไม่ขึ้น (NoSuchBeanDefinitionException) หรือถ้ามี Executor bean อื่นในระบบ
// (เช่นจาก @EnableAsync) จะได้ NoUniqueBeanDefinitionException แทน — ตั้งชื่อ method ให้ชัดเจนแบบนี้ช่วยแยกได้
@Bean
public Executor paymentExecutor() {
return Executors.newFixedThreadPool(25); // pool size ควรตรงกับ bulkhead.maxConcurrentCalls (25)
}💡 ทำไมต้อง
CompletableFuture? —@TimeLimiterต้องใช้ async type เพราะมันจะ cancel future เมื่อ timeout ถ้าใช้ blocking method ธรรมดา TimeLimiter จะ no-op (ไม่ทำงาน — ทำเหมือนไม่มี annotation อยู่เลย)
⚠️ ลำดับการทำงานของ annotation (สำคัญ!) — Resilience4j ห่อ method จากนอกเข้าใน ตามลำดับจริงตามเอกสาร Resilience4j:
Retry → CircuitBreaker → RateLimiter → TimeLimiter → Bulkhead → method (outer) (inner)ผลที่ตามมา: Retry อยู่นอกสุด → ถ้า downstream fail 3 ครั้ง CircuitBreaker จะเห็น 3 failures (ไม่ใช่ 1)
- เมื่อ tuning ค่า
failureRateThresholdต้องคำนึงถึง retry ด้วย- ถ้า CB OPEN แล้ว Retry จะไม่ช่วย (CB short-circuit ก่อน Retry ลอง)
- stacking หลาย annotation ทำให้ debug ยาก แนะนำให้เริ่มจาก 1-2 ตัวก่อน แล้วเพิ่มทีละตัวเมื่อจำเป็น
State ของ Circuit Breaker
- CLOSED = normal — request ผ่าน, นับ fail/success
- OPEN = fail fast — ไม่เรียกจริง, return fallback ทันที (or throw)
- HALF_OPEN = ทดลอง
permittedNumberOfCallsInHalfOpenStateครั้ง — ถ้าสำเร็จ → CLOSED, fail → OPEN
ดู state ผ่าน Actuator
yaml
management:
endpoints:
web:
exposure:
include: health,circuitbreakers,circuitbreakereventsGET /actuator/circuitbreakers → state ของทุก CB
7.4 ⚡ Chaos Engineering — ทดสอบ resilience จริง
ของ Netflix: Chaos Monkey random kill production instance
Spring มี Chaos Monkey for Spring Boot:
yaml
chaos:
monkey:
enabled: true
watcher:
service: true
assaults:
latencyActive: true
latencyRangeStart: 1000
latencyRangeEnd: 3000
exceptionsActive: true→ ทดสอบในขั้น staging: ระบบเรารับมือกับ latency spike + exception ได้จริงไหม?
Part 8: Distributed Data — Saga, Outbox, Idempotency
8.1 ทำไม Distributed Transaction ใช้ไม่ได้
java
// อย่าทำ ❌
@Transactional
public void createOrder() {
userService.deductBalance(...); // service A (HTTP)
orderRepo.save(...); // service B (local DB)
}@Transactional ทำงาน ข้าม service ไม่ได้ เพราะแต่ละ service มี DB ของตัวเอง
ทางเลือก XA (เอกซ์เอ — มาตรฐาน protocol สำหรับ distributed transaction) / 2PC (Two-Phase Commit):
- ทำงานได้ทางเทคนิค (Atomikos, Narayana — library ภายนอกสำหรับ distributed transaction ใน Java — ต้องเพิ่ม dependency เอง)
- แต่ scale ลำบาก, lock distributed นาน, complex
- ปี 2026 แทบไม่ใช้ — เพราะ throughput + availability แย่
💡 LRA (Long Running Actions / MicroProfile LRA) เป็น alternative ระหว่าง XA และ Saga เต็มรูปแบบ — คือมาตรฐานที่ให้ service ประกาศไว้ล่วงหน้าว่า "ถ้า action ที่ใช้เวลานานนี้ล้มเหลว จะ compensate (undo) ยังไง" โดยไม่ต้องเขียนกลไก Saga เต็มรูปแบบเอง — ใช้เมื่อต้องการ compensation แต่ไม่อยาก implement Saga เอง รองรับผ่าน Narayana LRA และ Spring extension
→ ทางออกสมัยใหม่: Saga + Outbox + Idempotency
8.2 Saga Pattern
Saga = sequence ของ local transaction (แต่ละ service commit ของตัวเอง) + compensation (undo logic) ถ้าขั้นไหน fail
มี 2 รูปแบบ:
A) Choreography — event-driven (decentralized)
- ✅ loose coupling — service ไม่รู้จักกัน, แค่ publish/subscribe event
- ✅ ไม่มี single point of failure
- ❌ debug ยาก — flow กระจาย, ต้องไล่ log/trace ทุก service
- ❌ อาจเกิด event loop ถ้าออกแบบไม่ดี
- ❌ business flow ไม่ explicit — อ่าน code service เดียวไม่เข้าใจทั้ง flow
B) Orchestration — central coordinator
- ✅ flow ชัด, debug ง่าย, มี state machine เดียวกลาง
- ✅ retry/timeout ที่ระดับ saga ทำง่าย
- ❌ orchestrator = single point ต้อง HA
- ❌ orchestrator รู้ business เยอะ — เสี่ยง god-class
💡 เลือกอะไรดี: flow ง่ายๆ → Choreography; flow ซับซ้อนหลาย step → Orchestration
Tool production ของ Orchestration: Temporal (พัฒนาต่อจาก Uber Cadence), Camunda 8, หรือ Spring Statemachine
8.3 Transactional Outbox — กัน "ทำ DB แล้ว publish ไม่สำเร็จ"
ปัญหาคลาสสิก:
java
@Transactional
public void createOrder(Order o) {
orderRepo.save(o); // commit DB
kafkaTemplate.send("orders", new OrderCreated(o.id())); // ⚠️ ถ้า crash ตรงนี้?
}ถ้า DB commit แล้ว service ตายก่อน Kafka send → DB มี order แต่ไม่มี event → ระบบ inconsistent ตลอดกาล (downstream service ไม่รู้)
ส่งกลับด้านก็แย่ — Kafka ส่งแล้ว DB rollback → consumer รับ event ของ order ที่ไม่มีจริงใน DB
ทางแก้ — Outbox Pattern
- ใน transaction เดียวกัน — เขียน event ลง outbox table (DB เดียวกัน) → atomic
- Relay process อ่าน outbox → publish ไป Kafka → mark sent
- ถ้า relay ตาย → restart → ส่ง event ที่ค้าง (เพราะยังไม่ mark sent)
sql
CREATE TABLE outbox (
id UUID PRIMARY KEY,
aggregate_type VARCHAR(64), -- 'Order', 'Payment'
aggregate_id VARCHAR(64),
event_type VARCHAR(64),
payload JSONB,
created_at TIMESTAMPTZ DEFAULT now(),
processed_at TIMESTAMPTZ
);
CREATE INDEX idx_outbox_unprocessed ON outbox (created_at) WHERE processed_at IS NULL;java
// Outbox entity (ย่อ)
@Entity
@Table(name = "outbox")
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Outbox {
@Id private UUID id;
private String aggregateType;
private String aggregateId;
private String eventType;
@Column(columnDefinition = "JSONB") private JsonNode payload;
private Instant createdAt; // ตั้งค่าใน @PrePersist ไม่ใช่ field initializer
private Instant processedAt;
@PrePersist
void onCreate() { this.createdAt = Instant.now(); }
}java
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepo;
private final OutboxRepository outboxRepo;
private final ObjectMapper mapper;
@Transactional
public Order create(CreateOrderRequest req) {
Order o = orderRepo.save(new Order(req));
outboxRepo.save(new Outbox(
UUID.randomUUID(),
"Order", o.getId().toString(),
"OrderCreated",
mapper.valueToTree(new OrderCreatedEvent(o))
));
return o; // ⭐ DB commit เดียว — atomic
}
}Relay (background):
⚠️ Simplified version — มี issue สำคัญ 3 อย่างที่ production ต้องแก้:
- Multi-instance race: ถ้ามีหลาย pod รัน
@Scheduledพร้อมกัน → poll row เดียวกัน → publish ซ้ำ ต้องล็อค row ด้วยSELECT ... FOR UPDATE SKIP LOCKED(PostgreSQL/MySQL 8+) หรือใช้ ShedLock- Transaction boundary + async callback:
kafkaTemplate.send(...).whenComplete(...)ทำงาน หลัง transaction ปิด →outboxRepo.save(ev)ใน callback อยู่นอก tx เดิม → mark sent ไม่ atomic กับการอ่าน- Long-running transaction: เวอร์ชันข้างล่าง
kafkaTemplate.send(...).get(5s)block อยู่ใน tx → row ถูกล็อคด้วยFOR UPDATEค้างไว้ตลอด 5 วินาที pod อื่นเลย skip ข้ามไป (SKIP LOCKED ช่วยได้ก็ต่อเมื่อ tx commit แล้ว) — ถ้า Kafka ช้า throughput ตกฮวบทางที่ถูกกว่า: (ก) แยกเป็น 2 short transaction — tx แรก SELECT + ล็อค row, publish ออกนอก tx, แล้ว tx ที่สอง UPDATE
processed_atหรือ (ข) ใช้ Kafka transactional producer +KafkaTransactionManagerเชื่อม tx ของ DB กับ tx ของ Kafka เข้าด้วยกัน · ของจริงควรใช้ Debezium CDC (ดูข้างล่าง) แทนเขียน relay เอง
java
// =============================================================================
// ⚠️ ตัวอย่างการศึกษาเท่านั้น — ห้ามใช้ใน production โดยไม่แก้ 3 issue ในกล่องเตือนด้านบน
// สำหรับ production จริง ให้ใช้ Debezium CDC (ดูหัวข้อ "Production: ใช้ Debezium" ด้านล่าง)
// =============================================================================
// relay() ต้องอยู่ใน @Component แยกต่างหาก (เช่น OutboxRelay)
// ไม่ใช่ class เดียวกับ OrderService — เพราะ @Scheduled เรียก Spring-managed bean โดยตรง
// ถ้าเรียก relay() จาก OrderService เอง (self-invocation) @Transactional จะไม่ทำงาน
@Scheduled(fixedDelay = 1000)
@Transactional
public void relay() {
// ✅ FOR UPDATE SKIP LOCKED — pod อื่นข้าม row ที่ลอคแล้วไปเอา row ถัดไป
List<Outbox> pending = outboxRepo.findUnprocessedTop100ForUpdateSkipLocked();
for (Outbox ev : pending) {
try {
// ⚠️ simplified — บรรทัดนี้ block อยู่ใน tx (issue #3 ในกล่องเตือนด้านบน)
// ใน production ให้แยกเป็น 2-transaction pattern หรือใช้ Debezium CDC แทน
kafkaTemplate.send(ev.getAggregateType(), ev.getAggregateId(), ev.getPayload())
.get(5, TimeUnit.SECONDS);
ev.setProcessedAt(Instant.now()); // mark sent ภายใน tx เดียวกัน
} catch (Exception e) {
log.error("Outbox publish failed id={}", ev.getId(), e);
// ปล่อยให้ tx commit row อื่นที่สำเร็จ — row นี้ค่อย retry รอบหน้า
// ⚠️ simplified: catch (Exception) กว้างเกิน — ครอบทั้ง error ชั่วคราวจาก Kafka
// และ bug จริงใน row (เช่น field เป็น null แล้ว NPE) รอบละ 1 วินาที (fixedDelay)
// ถ้า row เดิม fail ซ้ำตลอดจะได้ log สแปมรัวๆ ไม่มี backoff/DLQ
// production ควรแยก: error ชั่วคราว → retry ปกติ, error ซ้ำเกิน N ครั้ง → ย้ายไป DLQ/แจ้งเตือน แทนที่จะ log เฉยๆ
}
}
}Repository query:
⚠️ FOR UPDATE SKIP LOCKED ใน query ด้านล่างนี้ถูก ignore เงียบ ๆ บน H2 (in-memory DB ที่ใช้ใน dev/test) ทำให้ pod หลายตัว publish event ซ้ำได้ — ดูกล่อง 🔴 DB compatibility ด้านล่างสำหรับรายละเอียด
java
public interface OutboxRepository extends JpaRepository<Outbox, UUID> {
@Query(value = """
SELECT * FROM outbox
WHERE processed_at IS NULL
ORDER BY id
LIMIT 100
FOR UPDATE SKIP LOCKED
""", nativeQuery = true)
List<Outbox> findUnprocessedTop100ForUpdateSkipLocked();
}🔴 DB compatibility:
FOR UPDATE SKIP LOCKEDต้องการ PostgreSQL ≥ 9.5 หรือ MySQL ≥ 8.0 เท่านั้น · บน H2 (in-memory) คำสั่งนี้ ถูก ignore เงียบ ๆ ทำให้ pod หลายตัวล็อค row เดียวกันแล้ว publish ซ้ำ · บน MySQL 5.7 จะได้ syntax error ตรง ๆ · ถ้า DB ไม่รองรับ — เลือกอย่างใดอย่างหนึ่ง: ใช้ Debezium CDC (ดูข้างล่าง) หรือใช้ ShedLock เพื่อให้รัน relay loop ได้แค่ pod เดียวต่อช่วงเวลา
Production: ใช้ Debezium (CDC)
แทนที่จะเขียน relay เอง → ใช้ Debezium อ่าน Postgres WAL / MySQL binlog → push ไป Kafka อัตโนมัติ
- ✅ ไม่ต้องเขียน relay loop
- ✅ at-least-once จาก Debezium → Kafka (exactly-once end-to-end ต้องการ Kafka transactional producer + idempotent consumer เพิ่มเติม)
- ✅ low latency (~ms)
- ✅ ไม่ทำให้ DB หนัก (อ่าน WAL ไม่ใช่ poll table)
8.4 Idempotency — consumer ต้องรับ event ซ้ำได้
Kafka guarantee = at-least-once (อย่างน้อยหนึ่งครั้ง — Kafka รับประกันว่า event จะถูกส่งถึงผู้รับอย่างน้อย 1 ครั้ง แต่อาจมากกว่านั้น) → event อาจถูกส่งซ้ำ → consumer ต้อง idempotent
java
@KafkaListener(topics = "payments")
@Transactional
public void onPaymentCompleted(PaymentCompletedEvent ev) {
if (processedRepo.existsByEventId(ev.eventId())) {
return; // skip duplicate
}
orderService.markPaid(ev.orderId());
processedRepo.save(new ProcessedEvent(ev.eventId()));
}Idempotency key ใน HTTP API (สำหรับ POST/PUT):
text
POST /payments
Idempotency-Key: a8d4f2e1-...
{ "amount": 100, "orderId": 5 }Server เก็บ key + response — request ซ้ำ key เดิม → return cached response ไม่ทำใหม่
→ จะลึกเรื่อง messaging + Outbox ใน บทที่ 12: Messaging (Kafka + RabbitMQ)
📚 Chris Richardson — Microservices Patterns · Pat Helland — Idempotence Is Not a Medical Condition
Part 9: Communication Patterns
ไม่ใช่ทุก service ต้องคุยกันด้วย REST — แต่ละ pattern เหมาะกับ use case ต่างกัน
9.1 Sync vs Async
| Sync (REST/gRPC) | Async (Messaging/Event) | |
|---|---|---|
| Caller รอ response | ใช่ | ไม่ |
| Coupling | tight (caller (ผู้เรียก) รู้จัก callee (ผู้ถูกเรียก)) | loose (publisher ไม่รู้ subscriber) |
| Latency | fast (single hop — ไปถึงปลายทางรอบเดียว) | medium (queue + consumer) |
| Failure mode | error ตอบกลับทันที | retry/DLQ (Dead Letter Queue — คิวเก็บ message ที่ส่งซ้ำแล้วยังล้มเหลว เพื่อนำมาตรวจสอบภายหลัง) ในเบื้องหลัง |
| Use case | query, real-time | event, fire-and-forget (ยิงแล้วลืม — ส่งข้อมูลไปแล้วไม่รอผลลัพธ์), broadcast |
Rule of thumb:
- ต้องการ "ผลลัพธ์ทันที" + user รออยู่ → Sync
- "เกิดเหตุการณ์แล้วใครสนใจค่อยรับ" → Async
- Chain ยาว (A→B→C→D) → Async เพื่อกัน latency รวม + กัน cascade fail
🛡️ ถ้าจำเป็นต้อง sync chain — ต้องใส่ Circuit Breaker + Timeout ที่ทุก hop ของ chain (ใช้ Resilience4j — ดู Part 7) ไม่งั้น service ปลายสาย hang → ไล่ block ทั้ง chain → cascade failure
9.2 REST vs gRPC vs GraphQL
| REST | gRPC | GraphQL | |
|---|---|---|---|
| Protocol | HTTP/1.1 หรือ /2 + JSON | HTTP/2 + Protobuf (binary — รูปแบบข้อมูลที่เก็บเป็นเลขฐานสอง ไม่ใช่ตัวอักษรอ่านได้แบบ JSON ทำให้เล็กและเร็วกว่าแต่ debug ด้วยตาเปล่าไม่ได้) | HTTP + JSON, single endpoint |
| Schema | OpenAPI (optional) | Protobuf (required, code-gen) | SDL (Schema Definition Language — ภาษาสำหรับนิยาม schema ของ GraphQL) (required) |
| Performance | กลาง | เร็วที่สุด (binary, multiplexing — ส่งหลาย request พร้อมกันบน connection เดียว) | กลาง |
| Streaming | SSE (uni) / WebSocket | ✅ bi-directional native | ✅ (subscription) |
| Browser-friendly | ✅ | ❌ (ต้อง gRPC-Web proxy) | ✅ |
| ใช้ดีเมื่อ | public API, debug ง่าย | service-to-service ภายใน, low latency | client-driven query, mobile (ลด over-fetch) |
gRPC ใน Spring Boot
💡 ปี 2026 มีสองตัวเลือก: (1) net.devh (community starter ด้านล่าง — ใช้งานดีมานาน) หรือ (2) Spring gRPC (first-party จาก Spring team — https://spring.io/projects/spring-grpc เพิ่งเข้า Spring portfolio ปี 2024+) สำหรับโปรเจกต์ใหม่สามารถพิจารณา Spring gRPC ได้
xml
<!-- community starter — ต้องระบุ version เอง เพราะไม่อยู่ใน Spring Boot BOM -->
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>3.1.0.RELEASE</version> <!-- ⚠️ ตรวจสอบ version จริงที่ mvnrepository.com ก่อนใช้ — net.devh เปลี่ยนไปใช้ version แบบไม่มี .RELEASE suffix แล้ว (เช่น 3.1.0) การใช้ .RELEASE อาจหา artifact ไม่เจอบน Maven Central -->
</dependency>💡 ตัวอย่างด้านล่างใช้
UserServiceGrpc.UserServiceImplBaseซึ่ง generate มาจาก.protofile ผ่านprotobuf-maven-plugin— ต้องตั้งค่า plugin ก่อน build ถึงจะได้ Java class เหล่านี้ (ดูเอกสาร protobuf-maven-plugin หรือ Spring gRPC docs สำหรับ setup เต็ม)
protobuf
// user.proto
syntax = "proto3";
service UserService {
rpc GetUser(GetUserRequest) returns (User);
}
message GetUserRequest { int64 id = 1; }
message User { int64 id = 1; string name = 2; }java
@GrpcService
public class UserGrpcService extends UserServiceGrpc.UserServiceImplBase {
@Override
public void getUser(GetUserRequest req, StreamObserver<User> obs) {
User user = User.newBuilder().setId(req.getId()).setName("Alice").build();
obs.onNext(user);
obs.onCompleted();
}
}→ ทีมที่ services ภายใน traffic สูง (เช่น search, recommendation) มัก mix REST (สำหรับ public) + gRPC (สำหรับ internal)
GraphQL Federation
หลาย service มี GraphQL ของตัวเอง → Apollo Federation / Spring for GraphQL รวมเป็น schema เดียวให้ client → client ส่ง query เดียว, gateway query ไปแต่ละ subgraph (กราฟย่อย — GraphQL schema ของแต่ละ service)
9.3 Event-Driven Architecture
ทุก state change = event → broker (Kafka/Rabbit) → ใครสนใจ subscribe
- ✅ scale แนวนอน — เพิ่ม consumer ได้ตามต้องการ
- ✅ loose coupling — เพิ่ม consumer ใหม่ไม่ต้องแก้ publisher
- ❌ ต้องคิดเรื่อง ordering, idempotency, schema evolution
- ❌ debug ยาก — ใช้ tracing ช่วย
📚 บทที่ 12 จะลึกเรื่องนี้
Part 10: API Versioning + Contract Testing
10.1 ทำไม API Versioning สำคัญ
ระหว่าง deploy v2 ของ user-service:
- order-service ที่ใช้ v1 ยังต้องเรียกได้
- ห้าม breaking change กลาง production
Strategies
| Strategy | ตัวอย่าง | ดี | เสีย |
|---|---|---|---|
| URI path | /v1/users, /v2/users | เห็นชัด, cache ง่าย | URI เปลี่ยน — bookmark พัง |
| Header | Accept: application/vnd.myapp.v2+json | URI คงที่, REST-pure (ตรงตาม principle ของ REST architecture อย่างเคร่งครัด) | hidden, debug ยาก |
| Query param | /users?version=2 | ง่าย | ผสมกับ business param |
| Domain | v2.api.example.com | แยก infra ได้ | overhead จัดการ |
กฎทอง: Backward Compatibility
- เพิ่ม field — ทำได้ (client ที่ไม่รู้ field นี้แค่ ignore)
- เพิ่ม endpoint — ทำได้
- ลบ field / endpoint — breaking change → ต้อง v2
- เปลี่ยน type (string → int) — breaking change
- เปลี่ยน semantics — breaking change
Deprecation Cycle
text
Day 0: ปล่อย v2, v1 ยังใช้ได้ + header "Deprecation: true, Sunset: 2026-09-01"
Day 30: แจ้ง consumer ทุกทีมว่าต้อง migrate
Day 90: remove v110.2 Consumer-Driven Contract Testing (CDC)
ปัญหา: provider เปลี่ยน schema → consumer พังเงียบๆ จนเจอใน production
ทางแก้ — Consumer + Provider เห็น contract เดียวกัน, test ทั้ง 2 ฝั่งจาก contract
Tools
- Pact — แพร่หลายที่สุด, language-agnostic
- Spring Cloud Contract — fit ใน Spring ecosystem
Pact ตัวอย่าง
Consumer (order-service) เขียน expectation:
java
@PactConsumerTest
@PactTestFor(providerName = "user-service")
class UserClientPactTest {
@Pact(consumer = "order-service")
public RequestResponsePact getUser(PactDslWithProvider builder) {
return builder
.given("user 42 exists")
.uponReceiving("get user 42")
.path("/users/42").method("GET")
.willRespondWith()
.status(200)
.body(newJsonBody(b -> {
b.numberType("id", 42);
b.stringType("name", "Alice");
}).build())
.toPact();
}
@Test
void getUserById(MockServer mockServer) {
UserClient client = new UserClient(mockServer.getUrl());
assertThat(client.getById(42L).name()).isEqualTo("Alice");
}
}→ Pact file (JSON) จะถูก publish ไป Pact Broker (server กลางสำหรับเก็บและแชร์ contract file ระหว่างทีม — รันเองได้หรือใช้ PactFlow cloud) → provider download → verify ว่า service ตัวเองตอบตาม contract นี้ได้จริง
💡 contract test ≠ end-to-end test — เร็วกว่า (ไม่ต้องรันทั้งระบบ), เสถียรกว่า (mock ไม่พึ่ง network)
Part 11: Observability
ใน monolith — 1 request = 1 stack trace
ใน microservices — 1 request ผ่าน 5 service = stack trace 5 ตัว, log กระจาย → ต้องมีระบบ observability
3 เสาหลัก:
- Logs — text event, ใช้ debug
- Metrics — number aggregate, ใช้ alert/dashboard
- Traces — flow ของ request ข้าม service
11.1 OpenTelemetry — มาตรฐานเปิด
ก่อนหน้า: Zipkin (Twitter), Jaeger (Uber), OpenTracing, OpenCensus — แตกกัน
OpenTelemetry (OTel) = standard เปิดเดียวที่ CNCF (Cloud Native Computing Foundation — องค์กรที่ดูแลมาตรฐาน cloud-native) graduate (โปรเจกต์ที่ผ่านการรับรองว่า mature และพร้อมใช้งาน production), รวบ OpenTracing + OpenCensus, support ทุกภาษา/vendor
→ ส่งข้อมูลตาม OTLP ที่เดียว — backend เปลี่ยนได้โดยไม่ต้องแก้ code (OTLP = OpenTelemetry Protocol — รูปแบบมาตรฐานในการส่ง trace/metric/log ออกไปยังเครื่องมือเก็บข้อมูล ใช้ตัวเดียวส่งได้ทุก backend)
11.2 Setup Distributed Tracing
ใน microservices การ debug ต้องตาม request ข้ามหลาย service — เพิ่ม Micrometer Tracing + OTel exporter แล้ว Spring จะ propagate trace context และส่ง span ไป backend (Tempo/Zipkin) ให้อัตโนมัติ ทุก log ก็มี traceId ติดมาด้วย (รายละเอียดลึกอยู่ในหมวด observability):
xml
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>yaml
management:
tracing:
sampling:
probability: 1.0 # 100% (dev); prod ใช้ 0.05-0.1
otlp:
tracing:
endpoint: http://otel-collector:4318/v1/tracesส่ง request เข้า gateway → ดูใน Grafana Tempo, Jaeger, หรือ Zipkin:
text
Trace ID: a1b2c3
├── span: Gateway (5ms)
├── span: Order Service (20ms)
│ ├── span: User Service (8ms)
│ ├── span: Payment Service (15ms)
│ │ └── span: DB call (3ms)
│ └── span: Inventory Service (12ms)
└── total: 43ms11.3 Log + Trace ID
ใส่ trace_id ใน log → search ใน Loki/ELK เห็นทุก service ของ request เดียวกัน
yaml
logging:
pattern:
level: "%5p [${spring.application.name},%X{traceId:-},%X{spanId:-}]"Log:
INFO [user-service,abc123,def456] Loading user 42
INFO [order-service,abc123,xyz789] Creating order
ERROR [payment-service,abc123,qrs321] Charge failed→ ค้น abc123 ใน Loki → เห็นทุก log ที่เกี่ยวข้องกับ request นี้
💡 Grafana LGTM stack — Loki (log) + Grafana (UI) + Tempo (trace) + Mimir (metric) — open-source observability stack ที่นิยมปี 2026
11.4 Metrics — Micrometer + Prometheus
yaml
management:
endpoints:
web:
exposure:
include: prometheus,health,metrics
metrics:
tags:
application: ${spring.application.name}
env: ${spring.profiles.active:dev}Prometheus scrape /actuator/prometheus ของแต่ละ service → Grafana dashboard
Metric สำคัญ per service
| Metric | ความหมาย | ใช้ดู |
|---|---|---|
http_server_requests_seconds_count | request rate | RPS |
http_server_requests_seconds{quantile=0.99} | latency p99 | SLA |
http_server_requests_seconds_count{status=~"5.."} | error rate | SLO |
circuit_breaker_state{name="paymentService",state="open"} | CB open | alert |
feign_client_seconds | downstream call latency | bottleneck |
jvm_memory_used_bytes | memory | scale signal |
executor_pool_size_threads | thread pool | saturation |
RED Method + USE Method
- RED (services): Rate, Error, Duration
- USE (resources): Utilization, Saturation, Error
ตัวอย่าง mapping กับ metric จริง: Rate = http_server_requests_seconds_count (request/sec), Error = http_server_requests_seconds_count{status=~"5.."}, Duration = http_server_requests_seconds{quantile=0.99} (p99 latency)
→ ออกแบบ dashboard ตาม 2 กฎนี้ ครอบคลุมการสังเกตได้เกือบหมด
📚 บทที่ 5 (production-readiness) + folder
observability/มีรายละเอียดเพิ่ม
Part 12: Security ใน Microservices
12.1 Defense in Depth
ความปลอดภัยของ microservices ไม่พึ่งชั้นเดียว — ใช้หลักการ "defense in depth" ป้องกันเป็นชั้น ๆ: TLS ที่ขอบ, JWT validation ที่ gateway, mTLS ระหว่าง service, authorization ในแต่ละ service ถ้าชั้นหนึ่งถูกเจาะก็ยังมีชั้นอื่นกัน:
ในระบบ distributed ต้อง secure หลายชั้น:
12.2 JWT ที่ Gateway
ตามที่เห็นใน Part 5.5 — Gateway verify JWT ครั้งเดียว → ส่ง X-User-Id ไป downstream
Downstream เชื่อ header เพราะ network ภายในเป็น zone ที่ trusted (ในระบบเก่า)
ปัญหา: ถ้าใครเข้า network ภายในได้ → spoof X-User-Id ส่งไปตรงๆ ได้ → bypass auth
12.3 mTLS — Mutual TLS
TLS ปกติ = server พิสูจน์ตัวให้ client (HTTPS)
mTLS = ทั้ง 2 ฝั่งพิสูจน์ตัวกัน ด้วย certificate
text
Order Service ──[cert]──▶ Payment Service
◀─[cert]──→ Payment service รู้แน่ว่าเป็น order-service จริง (ไม่ใช่ใครก็ได้ที่อยู่ใน network)
Implement mTLS
วิธี A — ใน Spring Boot: config keystore/truststore เอง (เหนื่อย, ต้อง rotate cert)
วิธี B — ใช้ Service Mesh (Istio, Linkerd) → mesh จัดการ certificate (ใบรับรองดิจิทัลที่ใช้พิสูจน์ตัวตน) + rotate ให้อัตโนมัติ → app code ไม่รู้
→ วิธีนิยมปี 2026: Service Mesh (ดู Part 13)
12.4 Zero-Trust Networking
หลักการ: "ไม่ไว้ใจ network" — ทุก hop ต้อง auth
- ไม่มี "internal network ที่ปลอดภัย"
- ทุก service ต้อง verify identity ของ caller
- ใช้ mTLS + short-lived token + policy
ตัวอย่าง: SPIFFE/SPIRE — SPIFFE (Secure Production Identity Framework For Everyone) + SPIRE (ตัว implement) — ระบบออก digital identity ให้ workload แต่ละตัวใน cluster
📚 NIST SP 800-207 — Zero Trust Architecture
12.5 Service-to-Service Authorization
mTLS = "เป็นใคร" (authentication) — ยังต้องรู้ "ทำอะไรได้บ้าง" (authorization)
แนวทาง:
- JWT propagation — Gateway สร้าง internal JWT (claims เฉพาะ system) → service downstream verify
- OAuth2 Client Credentials — service A ขอ access token จาก auth server เพื่อเรียก service B
- OPA (Open Policy Agent) — เก็บ policy แยก, query เป็น Rego (เรโก — ภาษาสำหรับเขียน policy ของ OPA โดยเฉพาะ) policy
Part 13: Service Mesh — Layer ใหม่ของ Microservices
13.1 ปัญหาที่ Service Mesh แก้
ใน Spring Cloud — feature ทั้งหมดอยู่ใน library ของ app:
- Eureka client, Resilience4j, Sleuth tracing → ทุก service ต้อง dependency
- Multi-language → ต้องมี library ของแต่ละภาษา
- Upgrade tracing library → ต้อง redeploy ทุก service
Service Mesh ย้ายเรื่องเหล่านี้ไปอยู่ใน infrastructure layer (sidecar proxy)
App คุยกับ sidecar (localhost) → sidecar คุยกับ sidecar ของ service อื่น → ทำ mTLS, retry, timeout, tracing, metric ที่ infrastructure ไม่ใช่ที่ code
13.2 ตัวเลือก: Istio vs Linkerd vs Consul Connect vs Cilium
| Istio | Linkerd | Consul Connect | Cilium Service Mesh | |
|---|---|---|---|---|
| Data plane | Envoy (sidecar) | linkerd2-proxy (Rust, lightweight) | Envoy | eBPF (อีบีพีเอฟ — เทคโนโลยีที่รัน program ขนาดเล็กใน Linux kernel ได้อย่างปลอดภัย ช่วยทำ network monitoring/security โดยไม่ต้องมี sidecar — kernel level, sidecar-less option) |
| Complexity | สูง | ต่ำ | กลาง | กลาง |
| Performance overhead | medium | low | medium | very low (eBPF) |
| Feature | rich | minimal but solid | mid | network + L7 |
| Adoption ปี 2026 | mainstream แต่หนัก | นิยมในทีมเล็ก | ทีมที่ใช้ Consul อยู่แล้ว | กำลังโต (eBPF trend) |
Istio sample — VirtualService
yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: user-service
spec:
hosts: [user-service]
http:
- match:
- headers:
x-canary:
exact: "true"
route:
- destination:
host: user-service
subset: v2 # canary
- route:
- destination:
host: user-service
subset: v1
weight: 90
- destination:
host: user-service
subset: v2
weight: 10 # canary 10%→ canary deployment, traffic shifting, A/B test — ที่ infra ไม่ต้องแก้ code
Feature ที่ได้จาก Mesh "ฟรี"
- ✅ mTLS อัตโนมัติ (cert rotation auto)
- ✅ Retry, timeout, circuit breaker (config ที่ mesh ไม่ต้องใช้ Resilience4j)
- ✅ Distributed tracing (inject header อัตโนมัติ)
- ✅ Metric (golden metrics ทั้งหมด)
- ✅ Traffic splitting (canary, blue-green, A/B)
- ✅ Fault injection (chaos testing)
- ✅ Access control (policy ระดับ service)
13.3 Mesh vs Spring Cloud — เลือกอะไร?
| Concern | Spring Cloud | Service Mesh |
|---|---|---|
| Service discovery | Eureka | K8s DNS + mesh |
| Client-side LB | Spring Cloud LoadBalancer | mesh ทำที่ sidecar |
| Circuit breaker | Resilience4j (in-process) | mesh policy |
| Distributed tracing | Micrometer Tracing | mesh inject headers |
| Config | Spring Cloud Config | K8s ConfigMap + Vault |
| mTLS | manual | automatic |
| Multi-language | ต้องมี SDK ต่อภาษา | works for all (sidecar agnostic) |
| Business logic-level retry/fallback | ✅ ใน code ตรง | ❌ ทำที่ network ไม่รู้ business |
กฎ:
- Service Mesh = network-level concern (retry on 5xx, mTLS, traffic split)
- Spring Cloud / app code = business-level concern (fallback logic, business retry condition)
→ ใช้ทั้งคู่ — mesh เอาทำ transport-level, code เอาทำ business-level
Part 14: Deployment Patterns
14.1 Blue-Green Deployment
text
Blue (v1, รับ traffic) ◀── load balancer
Green (v2, idle)Deploy v2 ไป Green, smoke test → switch traffic → blue idle (rollback ง่าย แค่ switch กลับ)
- ✅ rollback ทันที
- ✅ no downtime
- ❌ ใช้ resource 2 เท่าชั่วคราว
14.2 Canary Release
ปล่อย v2 ให้ traffic 1% → ดู metric → 5% → 25% → 100%
- ✅ ลด blast radius ของ bug
- ✅ test กับ real traffic
- ❌ ต้อง observability ดี + automated rollback
K8s + Istio / Argo Rollouts / Flagger ทำ canary auto ได้
14.3 Feature Flags
แทนที่จะ deploy code ใหม่ → deploy code ที่มี flag ปิดอยู่ → enable flag ทีหลัง
java
@Component
@RequiredArgsConstructor
public class CheckoutService {
private final FeatureFlagClient flags;
public void checkout(Order o) {
if (flags.isEnabled("new-checkout-flow", o.userId())) {
newCheckoutFlow(o);
} else {
oldCheckoutFlow(o);
}
}
}Tool: Unleash, LaunchDarkly, Togglz
- ✅ แยก deploy (ขึ้น code ใหม่) ออกจาก release (เปิดให้ user ใช้) — สามารถ deploy ทุกวันแต่เปิด feature เฉพาะเมื่อพร้อม
- ✅ kill switch (ปิด feature ทันทีถ้าเจอ bug)
- ✅ progressive rollout per user/cohort
- ❌ code ซับซ้อนขึ้น (ต้องดูแล flag cleanup)
14.4 Rolling Update (default ของ K8s)
ทีละ Pod: ปิด v1 1 ตัว → start v2 1 ตัว → repeat → ครบทุก Pod
- ✅ no extra resource
- ✅ no downtime
- ❌ rollback ช้ากว่า blue-green
- ❌ ในช่วงเปลี่ยน — v1 + v2 อยู่พร้อมกัน → API ต้อง backward compatible
Part 15: Best Practices + Common Pitfalls
15.1 แบ่ง service ยังไง
กฎ: แบ่งตาม business boundary (Domain-Driven Design — Bounded Context)
| ❌ แบ่งตาม technical | ✅ แบ่งตาม business |
|---|---|
auth-service, db-service, cache-service | user-service, order-service, inventory-service |
แต่ละ service:
- มี DB ของตัวเอง (ห้าม share schema)
- มี API ที่ชัดเจน (contract)
- ทีมเดียวเป็น "owner"
- มี ownership ของ data (ไม่มี service อื่นแก้ DB นี้ได้)
📚 Eric Evans — Domain-Driven Design (Blue Book) · Vaughn Vernon — Implementing DDD
Service Size — เล็กแค่ไหนถึงพอดี
ไม่ได้วัดด้วย LOC — วัดด้วย:
- ทีมเดียวรับผิดชอบทั้งหมดได้ (2-pizza team rule — กฎทีม 2 ถาดพิซซ่าของ Amazon: ทีมที่ดีไม่ควรใหญ่เกินกว่าที่พิซซ่า 2 ถาดจะเลี้ยงอิ่มได้ ประมาณ 6-10 คน)
- deploy แยกได้จริง โดยไม่กระทบ service อื่น
- มี business capability ของตัวเอง
"If a microservice is so small that the cost of operating it exceeds its value, it's too small." (แปล: "ถ้า microservice เล็กจนต้นทุนการดูแลมันแพงกว่าคุณค่าที่ได้ แสดงว่ามันเล็กเกินไป" — อย่าแบ่งให้เล็กเกินจนดูแลไม่คุ้ม)
15.2 ตารางสรุป — Anti-Patterns ที่พบบ่อย
| Anti-Pattern | อาการ | แก้ |
|---|---|---|
| Distributed Monolith | service A เปลี่ยน → B ต้องเปลี่ยนตาม | ออกแบบ API ที่ stable, ใช้ contract test |
| Shared Database | 2 service อ่าน-เขียน table เดียวกัน | API call เท่านั้น — ไม่ share DB |
| Chatty Services | A เรียก B 10 รอบ ใน 1 request | denormalize / coarse-grained API / cache |
| Sync Chain ลึก | A→B→C→D (latency รวม + cascade fail) | ทำ async event-driven |
| No Service Discovery | hardcode URL | Eureka / K8s DNS / mesh |
| No Circuit Breaker | service หนึ่งล่ม → ทั้งระบบล่ม | Resilience4j |
| No Tracing | debug ไม่ได้ว่า request ช้าที่ไหน | OpenTelemetry + Tempo/Jaeger |
| Config ฝังใน Code | เปลี่ยน password ต้อง redeploy | Config Server / Vault / K8s Secret |
| Test เฉพาะ Unit | integration ใน prod พังเงียบ | + Contract test, Testcontainers, chaos |
| God Service | 1 service ทำทุกอย่าง (auth + order + user) | split ตาม business |
| No API Versioning | v2 deploy → consumer พัง | URI/header versioning + deprecation cycle |
| Manual Saga | เขียน compensation ผ่าน try-catch | ใช้ Temporal / Camunda |
| Forget Idempotency | retry → duplicate order | Idempotency key, processed event log |
| Lots of synchronous gRPC | retry storm (พายุ retry — เมื่อหลาย service retry พร้อมกัน ทำให้ traffic ระเบิดแบบทวีคูณ), cascade | bulkhead, timeout, circuit breaker |
| No Health Check | dead instance ยังรับ traffic | readiness/liveness probe |
15.3 Operational Excellence
Health Endpoint แบบที่ถูกต้อง
yaml
management:
endpoint:
health:
show-details: when-authorized
probes:
enabled: true
endpoints:
web:
exposure:
include: health,info,metrics,prometheus/actuator/health/liveness→ app ยังมีชีวิตอยู่? (failed → restart pod)/actuator/health/readiness→ พร้อมรับ traffic? (failed → remove from LB)
ห้ามรวม dependency external (DB, downstream service) ใน liveness — เพราะ DB down → ทุก pod restart loop (วนรีสตาร์ทซ้ำไม่หยุด) → ระบบยิ่งพัง
Graceful Shutdown
yaml
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s→ Pod terminate → หยุดรับ request ใหม่ → request ที่กำลังรันให้เสร็จก่อน (30 วินาที) → shutdown
Resource Limit
ตั้ง CPU/memory limit ใน K8s ทุก service → กัน "noisy neighbor"
Part 16: Roadmap สู่ขั้นสูง
จบบทแล้ว — ต่อไปอ่าน/ทำอะไร?
ขั้นถัดไป (intermediate)
- บทที่ 12 — Messaging (Kafka + RabbitMQ) — ลึก event-driven, Outbox, Streams
- บทที่ 10 — Reactive (WebFlux) — Spring Cloud Gateway ใช้ WebFlux เป็นพื้น
- บทที่ 11 — Caching — Redis, distributed cache
- บทที่ 13 — Batch + Scheduling — สำหรับงาน background
ขั้น Advanced — เลือกตามสายงาน
Architecture
- CQRS + Event Sourcing — แยก read/write model, เก็บประวัติด้วย event
- Domain-Driven Design ลึก — bounded context, aggregate, ubiquitous language
- Hexagonal / Clean Architecture — แยก domain ออกจาก infrastructure
- 📚 Sam Newman — Building Microservices, 2nd ed.
- 📚 Chris Richardson — Microservices Patterns
Infrastructure / Platform
- Kubernetes ลึก — operator, controller, CRD (devops/)
- Service Mesh ลึก — Istio, Linkerd, Cilium (devops/)
- GitOps — ArgoCD, Flux
- Infrastructure as Code — Terraform, Pulumi
- Progressive Delivery — Argo Rollouts, Flagger
Observability
- OpenTelemetry — instrumentation, propagation, sampling
- eBPF observability — Pixie, Cilium Hubble
- Grafana LGTM stack — Loki + Grafana + Tempo + Mimir
- 📚 Cindy Sridharan — Distributed Systems Observability
Data
- Debezium / CDC — change data capture
- Kafka Streams / ksqlDB — stream processing
- Polyglot persistence — DB ที่เหมาะกับแต่ละ service (RDBMS, Mongo, Cassandra, Elastic)
Reliability
- Chaos Engineering — Chaos Mesh, Litmus, Gremlin
- SRE practices — SLO, error budget, on-call (observability/)
- 📚 Google SRE Book (free online)
Workflow Orchestration
- Temporal — durable execution, workflow as code
- Camunda 8 — BPMN workflow
- Cadence (Uber, ก่อน Temporal)
Part 17: Checkpoint
🛠️ Checkpoint 8.1 — Eureka + 2 service
สร้าง:
- Eureka server (port 8761)
user-service(port 8081) — register กับ Eureka, มีGET /users/{id}order-service(port 8082) — register + เรียก user-service ผ่าน HTTP Interface
🛠️ Checkpoint 8.2 — API Gateway
เพิ่ม Gateway (port 8080):
/api/users/**→ user-service/api/orders/**→ order-service- Custom filter ที่ log ทุก request + verify JWT
🛠️ Checkpoint 8.3 — Circuit Breaker + Bulkhead
ใน order-service เพิ่ม @CircuitBreaker + @Bulkhead ตอนเรียก user-service
ถ้า user-service down → fallback return cached user
เปิด /actuator/circuitbreakers ดู state
🛠️ Checkpoint 8.4 — Distributed Tracing
ติด OpenTelemetry → ส่ง trace ไป Tempo/Jaeger (รัน Docker)
ส่ง request เข้า gateway → ดู trace ครอบ gateway → order → user
🛠️ Checkpoint 8.5 — Outbox + Kafka (กลับมาทำหลังอ่านบทที่ 12 — บทนี้ยังไม่ได้สอน Kafka setup)
เพิ่ม outbox table ใน order-service
เขียน OrderCreated event → relay → Kafka (ต้องเพิ่ม spring-kafka dependency + รัน Kafka ผ่าน Docker — ดูบทที่ 12)
เพิ่ม notification-service consume event → log
🛠️ Checkpoint 8.6 — Contract Testing
เขียน Pact test ใน order-service สำหรับการเรียก user-service
รัน provider verification ใน user-service
🛠️ Checkpoint 8.7 — (Advanced) Canary บน K8s + Istio
Deploy 2 version ของ user-service บน K8s
ใช้ Istio VirtualService split traffic 90/10
เพิ่ม metric ใน Grafana → ดูทั้ง 2 version
Part 18: สรุปบท
✅ Microservices ไม่ใช่ goal — เป็น consequence ของ scale / team / domain complexity
✅ Start with modular monolith → split เมื่อจำเป็นจริง
✅ 12-Factor App = พื้นฐานที่ทุก service ต้องตาม
✅ CAP + Eventual Consistency — ต้องออกแบบให้ UX รองรับ inconsistency ชั่วคราว
✅ Spring Cloud toolkit: Eureka, Gateway, Config, OpenFeign/HTTP Interface, Resilience4j, Vault, Contract
✅ K8s + Service Mesh — ปี 2026 ทับซ้อนกับ Spring Cloud หลายอย่าง — เลือกตามบริบท
✅ HTTP Interface (Spring 6) กำลังแทน OpenFeign สำหรับโปรเจกต์ใหม่
✅ Resilience: Circuit Breaker + Retry + Timeout + Bulkhead + Rate Limiter — ใช้คู่กัน
✅ ห้าม share DB ระหว่าง service — ใช้ Saga + Outbox + Idempotency แทน distributed transaction
✅ OpenTelemetry = standard ของ observability (trace + metric + log)
✅ Security: TLS ที่ edge + mTLS ที่ internal + zero-trust mindset
✅ Deployment: blue-green, canary, feature flag — separate deploy จาก release
✅ Anti-pattern ที่ต้องระวังที่สุด: Distributed Monolith — มี complexity ของทั้ง 2 ฝั่ง
💬 ปิดท้าย: Microservices เป็น ตัวเลือก, ไม่ใช่คำตอบสำหรับทุกปัญหา ทีมที่ประสบความสำเร็จไม่ใช่เพราะ "ใช้ microservices" แต่เพราะ "ใช้เครื่องมือที่ถูกกับบริบทของตัวเอง" — เริ่มจากเข้าใจปัญหา ไม่ใช่เริ่มจากเครื่องมือ