Skip to content

บทที่ 8 — Microservices (สถาปัตยกรรมแบบบริการเล็ก ๆ หลายตัว) + Spring Cloud (ฉบับครบเครื่อง 2026)

← บทที่ 7 | สารบัญ | บทที่ 9 →

หลังจากแอปของคุณโตขึ้น 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 MonolithMonolith ที่แบ่ง 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
mTLSmutual 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

MonolithMicroservices
Deployครั้งเดียวจบหลายครั้ง, ต้อง CI/CD จริงจัง
Scalescale ทั้งก้อนscale เฉพาะ service ที่ต้องการ
Tech stack1 ภาษาแต่ละ service เลือกได้ (Java + Go + Python)
Database1 DBDB แยกต่อ service
Team1 team หรือหลายทีมในที่เดียวหลาย team ทำงานอิสระจริง
Complexityต่ำ–กลางสูงมาก — network, partial failure, distributed tx
Latencyfunction call (ns)network call (ms)
Debugstack trace เดียวต้องใช้ distributed tracing
Testingunit + 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ความหมาย
1Codebase (ฐานโค้ด)1 repo ต่อ service, deploy ได้หลาย environment
2Dependencies (ของที่ต้องใช้)ประกาศชัดใน pom.xml/build.gradle, ไม่ใช้ system-wide
3Config (การตั้งค่า)เก็บใน environment variable, ไม่ฝังใน code
4Backing services (บริการเสริม)DB, cache, queue = resource ที่ attach ได้ผ่าน URL
5Build, release, run (สร้าง/ปล่อย/รัน)แยก 3 stage ชัด
6Processes (กระบวนการ)stateless (ไม่เก็บ state ไว้ใน process เอง) — state เก็บใน DB/cache แทน
7Port binding (ผูกพอร์ต)บริการตัวเองผ่าน port (ไม่พึ่ง container web server)
8Concurrency (รันพร้อมกัน)scale แนวนอน (หลาย process)
9Disposability (พร้อมทิ้ง)start/stop เร็ว, graceful shutdown
10Dev/prod parity (dev = prod)dev/staging/prod เหมือนกันมากที่สุด
11Logs (บันทึก)log เป็น stream (stdout), ไม่ใช่ไฟล์ — ให้ infra เก็บ
12Admin 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 (อาจเจอติดลบชั่วคราว)"

CPAP
ทำงานเมื่อ 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 ไม่ตรงกันชั่วคราว แต่จะตรงในที่สุด"

ตัวอย่าง:

  1. User ชำระเงิน → Payment Service บันทึก "paid"
  2. ต้องแจ้ง Order Service ว่า paid แล้ว → ส่ง event
  3. ระหว่างที่ event ยังไม่ถึง — Order Service ยังเห็น "pending" → inconsistent ชั่วคราว
  4. หลังจาก 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 GatewayAPI Gateway (reactive)✅ ใช้แพร่หลาย
Spring Cloud Gateway MVC (ใหม่ 2024+)API Gateway (servlet)✅ ทางเลือกใหม่ สำหรับทีมที่ไม่อยาก reactive

B) Communication + Resilience — Part 4, 7

Componentทำอะไรยุค 2026 ยังใช้?
Spring Cloud LoadBalancerClient-side LB (แทน Ribbon ที่ deprecated แล้ว)
OpenFeignHTTP client declarative🟡 ยังใช้งานได้ดีกับ Spring Boot 3.x แต่โปรเจกต์ใหม่แนะนำ HTTP Interface (@HttpExchange) แทน
Resilience4jCircuit Breaker, Retry, Bulkhead, Rate Limiter
Spring Cloud StreamEvent-driven abstraction (Kafka/Rabbit)🟡 ใช้ Kafka SDK ตรงๆ ได้เช่นกัน

C) Operations: Config + Secret + Observability + Contract — Part 6, 10, 11

Componentทำอะไรยุค 2026 ยังใช้?
Spring Cloud ConfigCentralized config🟡 K8s ConfigMap/Secret + Vault แทนได้
Spring Cloud VaultSecret management
Micrometer + OpenTelemetryMetrics + Tracing✅ standard
Spring Cloud KubernetesBridge ไป K8s primitive✅ ในระบบ K8s
Spring Cloud ContractConsumer-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)
ใครเลือก instanceclientproxy/LB
Resolve ที่ไหนapplication codeinfrastructure
ตัวอย่างEureka + Spring Cloud LoadBalancerK8s 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 DNSbuilt-inไม่ต้องลง registry แยก ถ้าอยู่บน K8s แล้ว (ทางเลือกหลักปี 2026)
Consul (HashiCorp)activehealth check + KV (key-value store) + multi-DC (หลาย data center) + ACL (Access Control List — ควบคุมสิทธิ์การเข้าถึง) ในตัว
Nacos (Alibaba)activeservice discovery + config center รวมตัวเดียว
Eureka 1.xmaintenance — ปลอดภัยใช้ต่อแต่ไม่มี feature ใหม่ecosystem Spring Cloud Netflix ครบ — เหมาะกับการ migrate ระบบเก่า
etcdactive, 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: false

Run → เปิด 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:8083

3.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 ทำงาน:

  1. resolve user-service → ดูใน Eureka → ได้ list ของ instance live
  2. เลือก 1 instance (default = round-robin; เปลี่ยนเป็น weighted/random/health-aware ได้)
  3. ส่ง 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 ใหม่อัตโนมัติ
EurekaK8s DNSService Mesh (Istio)
ทำงานที่ไหนapplicationinfrastructure (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)
ตำแหน่งปี 2026non-K8s deployment / on-premmainstreamทีมที่ต้องการ 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

OpenFeignSpring HTTP Interface
Maintained byOpenFeign + Spring Cloud teamSpring core team
Annotation@FeignClient, @GetMapping@GetExchange, @PostExchange
Default underlying clientOkHttp / Apache HttpClient (library สำหรับส่ง HTTP request ของ Java)RestClient / WebClient
Reactivepartial (need extension)✅ ตรงๆ ผ่าน WebClient
ต้องการ Spring Cloud❌ (อยู่ใน Spring core)
Decoder/encodercustom-ize ผ่าน configใช้ของ Spring (Jackson, ฯลฯ) ตรงๆ
โครงการใหม่ปี 2026ใช้ได้แนะนำใช้

ทำไมแนะนำ HTTP Interface: เป็นส่วนหนึ่งของ Spring core, integrate กับ observability stack, type-safe กว่าเดิม, รองรับทั้ง blocking + reactive ตรงๆ ผ่าน builder เดียวกัน

📚 Spring Framework — HTTP Interface docs


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-Id header
  • 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/1
  • GET /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
StackWebFlux + NettyServlet + Tomcat
Throughput สูงสุดสูงกว่า (non-blocking)กลาง
ใช้เมื่อhigh concurrency, streamingทีมยังไม่ถนัด reactive
Debugreactive 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.com

Filter = "ทำอะไรก่อน/หลัง forward"

yaml
filters:
  - StripPrefix=1
  - AddRequestHeader=X-API-Version, v2
  - AddResponseHeader=X-Service, gateway
  - RewritePath=/api/(?<segment>.*), /$\{segment}
  - CircuitBreaker=userServiceCB
  - RequestRateLimiter=...
  - RemoveRequestHeader=Cookie
  - Retry=3

5.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 ตามชื่อ service

Git 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.yml

6.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-service

Secret ที่ 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 ConfigK8s ConfigMap + SecretVault
VersioningGit historyไม่มี (ต้อง GitOps)Built-in
Secret encryptionผ่าน Jasypt / Vaultbase64 (การเข้ารหัสข้อมูลให้เป็นตัวอักษร — ไม่ใช่การเข้ารหัสเพื่อความปลอดภัย ใครก็ decode กลับได้ง่าย ๆ)✅ encrypt at rest
Hot reload@RefreshScope + Busrestart 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 ที่ส่งออก → ไม่ถล่ม downstreamResilience4j 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,circuitbreakerevents

GET /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

  1. ใน transaction เดียวกัน — เขียน event ลง outbox table (DB เดียวกัน) → atomic
  2. Relay process อ่าน outbox → publish ไป Kafka → mark sent
  3. ถ้า 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 ต้องแก้:

  1. Multi-instance race: ถ้ามีหลาย pod รัน @Scheduled พร้อมกัน → poll row เดียวกัน → publish ซ้ำ ต้องล็อค row ด้วย SELECT ... FOR UPDATE SKIP LOCKED (PostgreSQL/MySQL 8+) หรือใช้ ShedLock
  2. Transaction boundary + async callback: kafkaTemplate.send(...).whenComplete(...) ทำงาน หลัง transaction ปิด → outboxRepo.save(ev) ใน callback อยู่นอก tx เดิม → mark sent ไม่ atomic กับการอ่าน
  3. 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)

📚 Debezium Outbox event router


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ใช่ไม่
Couplingtight (caller (ผู้เรียก) รู้จัก callee (ผู้ถูกเรียก))loose (publisher ไม่รู้ subscriber)
Latencyfast (single hop — ไปถึงปลายทางรอบเดียว)medium (queue + consumer)
Failure modeerror ตอบกลับทันทีretry/DLQ (Dead Letter Queue — คิวเก็บ message ที่ส่งซ้ำแล้วยังล้มเหลว เพื่อนำมาตรวจสอบภายหลัง) ในเบื้องหลัง
Use casequery, real-timeevent, 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

RESTgRPCGraphQL
ProtocolHTTP/1.1 หรือ /2 + JSONHTTP/2 + Protobuf (binary — รูปแบบข้อมูลที่เก็บเป็นเลขฐานสอง ไม่ใช่ตัวอักษรอ่านได้แบบ JSON ทำให้เล็กและเร็วกว่าแต่ debug ด้วยตาเปล่าไม่ได้)HTTP + JSON, single endpoint
SchemaOpenAPI (optional)Protobuf (required, code-gen)SDL (Schema Definition Language — ภาษาสำหรับนิยาม schema ของ GraphQL) (required)
Performanceกลางเร็วที่สุด (binary, multiplexing — ส่งหลาย request พร้อมกันบน connection เดียว)กลาง
StreamingSSE (uni) / WebSocket✅ bi-directional native✅ (subscription)
Browser-friendly❌ (ต้อง gRPC-Web proxy)
ใช้ดีเมื่อpublic API, debug ง่ายservice-to-service ภายใน, low latencyclient-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 มาจาก .proto file ผ่าน 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 พัง
HeaderAccept: application/vnd.myapp.v2+jsonURI คงที่, REST-pure (ตรงตาม principle ของ REST architecture อย่างเคร่งครัด)hidden, debug ยาก
Query param/users?version=2ง่ายผสมกับ business param
Domainv2.api.example.comแยก infra ได้overhead จัดการ

กฎทอง: Backward Compatibility

  • เพิ่ม field — ทำได้ (client ที่ไม่รู้ field นี้แค่ ignore)
  • เพิ่ม endpoint — ทำได้
  • ลบ field / endpointbreaking change → ต้อง v2
  • เปลี่ยน type (string → int) — breaking change
  • เปลี่ยน semanticsbreaking change

Deprecation Cycle

text
Day 0:   ปล่อย v2, v1 ยังใช้ได้ + header "Deprecation: true, Sunset: 2026-09-01"
Day 30:  แจ้ง consumer ทุกทีมว่าต้อง migrate
Day 90:  remove v1

10.2 Consumer-Driven Contract Testing (CDC)

ปัญหา: provider เปลี่ยน schema → consumer พังเงียบๆ จนเจอใน production

ทางแก้ — Consumer + Provider เห็น contract เดียวกัน, test ทั้ง 2 ฝั่งจาก contract

Tools

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 เสาหลัก:

  1. Logs — text event, ใช้ debug
  2. Metrics — number aggregate, ใช้ alert/dashboard
  3. 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: 43ms

11.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_countrequest rateRPS
http_server_requests_seconds{quantile=0.99}latency p99SLA
http_server_requests_seconds_count{status=~"5.."}error rateSLO
circuit_breaker_state{name="paymentService",state="open"}CB openalert
feign_client_secondsdownstream call latencybottleneck
jvm_memory_used_bytesmemoryscale signal
executor_pool_size_threadsthread poolsaturation

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

IstioLinkerdConsul ConnectCilium Service Mesh
Data planeEnvoy (sidecar)linkerd2-proxy (Rust, lightweight)EnvoyeBPF (อีบีพีเอฟ — เทคโนโลยีที่รัน program ขนาดเล็กใน Linux kernel ได้อย่างปลอดภัย ช่วยทำ network monitoring/security โดยไม่ต้องมี sidecar — kernel level, sidecar-less option)
Complexityสูงต่ำกลางกลาง
Performance overheadmediumlowmediumvery low (eBPF)
Featurerichminimal but solidmidnetwork + L7
Adoption ปี 2026mainstream แต่หนักนิยมในทีมเล็กทีมที่ใช้ 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 — เลือกอะไร?

ConcernSpring CloudService Mesh
Service discoveryEurekaK8s DNS + mesh
Client-side LBSpring Cloud LoadBalancermesh ทำที่ sidecar
Circuit breakerResilience4j (in-process)mesh policy
Distributed tracingMicrometer Tracingmesh inject headers
ConfigSpring Cloud ConfigK8s ConfigMap + Vault
mTLSmanualautomatic
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-serviceuser-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 Monolithservice A เปลี่ยน → B ต้องเปลี่ยนตามออกแบบ API ที่ stable, ใช้ contract test
Shared Database2 service อ่าน-เขียน table เดียวกันAPI call เท่านั้น — ไม่ share DB
Chatty ServicesA เรียก B 10 รอบ ใน 1 requestdenormalize / coarse-grained API / cache
Sync Chain ลึกA→B→C→D (latency รวม + cascade fail)ทำ async event-driven
No Service Discoveryhardcode URLEureka / K8s DNS / mesh
No Circuit Breakerservice หนึ่งล่ม → ทั้งระบบล่มResilience4j
No Tracingdebug ไม่ได้ว่า request ช้าที่ไหนOpenTelemetry + Tempo/Jaeger
Config ฝังใน Codeเปลี่ยน password ต้อง redeployConfig Server / Vault / K8s Secret
Test เฉพาะ Unitintegration ใน prod พังเงียบ+ Contract test, Testcontainers, chaos
God Service1 service ทำทุกอย่าง (auth + order + user)split ตาม business
No API Versioningv2 deploy → consumer พังURI/header versioning + deprecation cycle
Manual Sagaเขียน compensation ผ่าน try-catchใช้ Temporal / Camunda
Forget Idempotencyretry → duplicate orderIdempotency key, processed event log
Lots of synchronous gRPCretry storm (พายุ retry — เมื่อหลาย service retry พร้อมกัน ทำให้ traffic ระเบิดแบบทวีคูณ), cascadebulkhead, timeout, circuit breaker
No Health Checkdead instance ยังรับ trafficreadiness/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)

  1. บทที่ 12Messaging (Kafka + RabbitMQ) — ลึก event-driven, Outbox, Streams
  2. บทที่ 10Reactive (WebFlux) — Spring Cloud Gateway ใช้ WebFlux เป็นพื้น
  3. บทที่ 11Caching — Redis, distributed cache
  4. บทที่ 13Batch + Scheduling — สำหรับงาน background

ขั้น Advanced — เลือกตามสายงาน

Architecture

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

Data

  • Debezium / CDC — change data capture
  • Kafka Streams / ksqlDB — stream processing
  • Polyglot persistence — DB ที่เหมาะกับแต่ละ service (RDBMS, Mongo, Cassandra, Elastic)

Reliability

Workflow Orchestration

  • Temporal — durable execution, workflow as code
  • Camunda 8 — BPMN workflow
  • Cadence (Uber, ก่อน Temporal)

Part 17: Checkpoint

🛠️ Checkpoint 8.1 — Eureka + 2 service
สร้าง:

  1. Eureka server (port 8761)
  2. user-service (port 8081) — register กับ Eureka, มี GET /users/{id}
  3. 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" แต่เพราะ "ใช้เครื่องมือที่ถูกกับบริบทของตัวเอง" — เริ่มจากเข้าใจปัญหา ไม่ใช่เริ่มจากเครื่องมือ


← บทที่ 7 | สารบัญ | บทที่ 9 → Spring MVC ลึก