โหมดมืด
บทที่ 14 — Data Management (DB per Service + CQRS + Event Sourcing + Data Mesh)
← บทที่ 13: Resilience | สารบัญ | บทที่ 15: Tracing + Correlation →
TL;DR: บทนี้ตอบ "data ใน microservices จัดยังไงเมื่อไม่มี global transaction" — DB per Service (no sharing), Polyglot Persistence, CQRS (แยก read/write), Event Sourcing (event = source of truth), Dual-write trap + Postgres logical replication, Expand-Contract zero-downtime migration, Data Mesh (org-level). ข้ามได้ถ้า: ทำ CQRS + ES เป็น + handle dual-write ได้
ใน microservices — data ไม่ใช่ "1 DB ตอบทุกอย่าง" อีกต่อไป
📖 กางศัพท์ตั้งแต่ต้นบท (ครั้งเดียว — ไม่ต้องเลื่อนหา):
- CRUD (Create/Read/Update/Delete = สร้าง/อ่าน/อัปเดต/ลบ — operation พื้นฐานที่ทุก DB มี)
- DB per service = ทุก service มี DB ของตัวเอง, ห้าม service อื่น query ตรง — เป็นหลักการพื้นฐานของ microservices
- CQRS (Command Query Responsibility Segregation — แยก model "เขียน" กับ "อ่าน" ออกจากกัน)
- Event Sourcing = เก็บเหตุการณ์ที่เกิด (event) เป็น source of truth แทนการเก็บ state ปัจจุบัน
- eventual consistency (ความสอดคล้องในที่สุด — ข้อมูลระหว่าง service อาจไม่ตรงกันทันที แต่จะตรงกันในที่สุด เมื่อ event สำเร็จทั้งหมด) ตรงข้ามกับ strong consistency (ความสอดคล้องทันที — ทุก client เห็นข้อมูลตรงกันทันทีหลังเขียน เหมือน SELECT ใน DB เดียวกัน อย่างที่ RDBMS transaction ทำได้)
- CAP theorem (ทฤษฎี CAP = Consistency / Availability / Partition tolerance — ใน distributed system เลือกได้แค่ 2 จาก 3)
- distributed transaction (transaction ข้าม service — ต้องการ atomic ข้าม DB หลายตัว ทำได้ยาก; 2PC = Two-Phase Commit = commit 2 ขั้น prepare + commit, ช้าและเปราะ)
- idempotency-key (กุญแจกัน duplicate — header ที่ client ส่งมาเพื่อให้ server dedup ตัวเองเวลา retry; ดูบท 03 REST + gRPC สำหรับรายละเอียด)
- WAL (Write-Ahead Log = บันทึกล่วงหน้าก่อนเขียนจริง — ทุก DB เขียน log ก่อน apply การเปลี่ยนแปลง เพื่อ crash recovery + replication)
- CDC (Change Data Capture — จับการเปลี่ยนแปลงของ DB ส่งออกเป็น event); Debezium (ดิ-เบ-เซียม — tool ของ Red Hat ที่อ่าน DB log/WAL แล้วส่งเป็น event ใน Kafka)
- sharding (ชาร์-ดิ้ง = การแบ่งข้อมูลเป็นชิ้น — กระจาย data ข้าม DB หลายเครื่องโดยใช้ shard key เช่น
user_id mod N)
บทนี้ครอบคลุม:
- DB per Service (ปฏิเสธ shared DB)
- Polyglot Persistence (ใช้หลาย DB ตาม use case)
- CQRS (แยก read/write model)
- Event Sourcing (ลึก)
- Data Mesh (organization-level)
- Reporting + Analytics ใน microservices
- 🆕 Dual-write trap — กับดักที่ทุก microservices เจอ
- 🆕 Postgres logical replication + Debezium 3.x — pattern modern (2026 baseline)
- 🆕 Zero-downtime migration — Expand-Contract + dual-read/write
ใช้เวลา 3-4 ชั่วโมง
Part 1: DB per Service
1.1 หลักการ
ทบทวนจากบท 01 (สรุปสั้น: Bounded Context = ขอบเขตที่ชัดเจนของ domain หนึ่ง — กำหนดว่า service นี้รับผิดชอบข้อมูลอะไรบ้าง ดูรายละเอียดเพิ่มเติมในบท 01):
- ทุก service own DB ของตัวเอง
- service อื่นเข้าถึงได้ผ่าน API หรือ Event เท่านั้น
❌ ทำไม shared DB ผิด: การแก้ schema (เช่น
ALTER TABLE orders ADD COLUMN) กระทบทุก service ที่ใช้ table นั้นพร้อมกัน → ต้อง coordinate release ระหว่างทีม → ไม่ "independently releasable" อีกต่อไป = ไม่ใช่ microservices จริง
1.2 ทำไม
- Independent schema evolution (schema evolution = การเปลี่ยน schema เช่น add/remove/rename column; independent = แก้ของทีมฉันได้โดยไม่ทำให้ทีมอื่นพัง) — แก้ table ของฉัน ไม่กระทบทีมอื่น
- Independent scaling — DB ใหญ่ตามต้องการของ service
- Polyglot persistence — ใช้ DB type ต่างกันได้
- Encapsulation — internal structure ซ่อน
1.3 ต้นทุน
- ไม่มี cross-service JOIN — ต้อง denormalize หรือ compose
- ไม่มี global transaction → ใช้ Saga
- Reporting ที่ต้องการข้อมูลข้าม service → ต้อง aggregate
Part 2: Polyglot Persistence
แต่ละ service ใช้ DB ที่เหมาะกับ workload ตัวเอง:
💡 มือใหม่ไม่ต้องตกใจกับชื่อ DB เยอะ ๆ ในตาราง — รู้จัก Postgres + MongoDB + Redis ก็พอเริ่มต้นได้ ตัวอื่น (Cassandra, ScyllaDB, ClickHouse, Pinecone, Weaviate, ...) คือทางเลือกเมื่อมี requirement เฉพาะ (volume สูงมาก, vector search, time-series ฯลฯ) — ไม่ต้องเรียนทุกตัว
| Use case | DB choice |
|---|---|
| Transactional CRUD | PostgreSQL, MySQL |
| Document / flexible schema | MongoDB |
| Time-series | TimescaleDB, InfluxDB |
| Graph relationship | Neo4j, Amazon Neptune |
| Search | Elasticsearch, OpenSearch, Meilisearch |
| Cache | Redis |
| Wide-column / big data | Cassandra, ScyllaDB, BigTable |
| Distributed SQL / NewSQL (global ACID — Atomicity/Consistency/Isolation/Durability คุณสมบัติของ transaction ที่รับประกันข้อมูลถูกต้องแม้เกิด error/crash — + horizontal scale) | CockroachDB 24.x, Google Spanner, FoundationDB, Aurora DSQL (AWS, ตรวจสอบความพร้อม/feature ล่าสุดก่อนใช้จริง) |
| OLAP / analytics | ClickHouse, BigQuery, Snowflake |
| Vector (AI/ML) | Pinecone, pgvector, Weaviate |
| Event store | EventStoreDB, Kafka |
| Object | S3, GCS, MinIO |
💡 NewSQL/Distributed SQL ในปี 2026: ถ้าเคยฝัน "อยาก ACID เหมือน Postgres แต่ scale ข้าม region ได้" — กลุ่มนี้ตอบโจทย์: CockroachDB (Postgres wire-compatible), Spanner (Google managed), Aurora DSQL (AWS serverless distributed Postgres — ตรวจสอบ feature/region ล่าสุดก่อนใช้จริง เพราะ product ใหม่มัก compatibility subset ไม่เท่า Postgres เต็มรูปแบบ). Trade-off: write latency สูงกว่า single-node Postgres เพราะต้อง consensus (Raft/Paxos) ข้าม node. เริ่มที่ Postgres ก่อน ย้ายเมื่อชน scaling wall จริง (scaling wall = กำแพงขยาย — จุดที่ระบบปัจจุบันรับ load เพิ่มไม่ได้อีกแม้จะเพิ่ม hardware)
→ Order Service ใช้ Postgres, Search Service ใช้ Elasticsearch, Cache Service ใช้ Redis
2.1 ⚠️ ระวัง polyglot abuse
หลาย DB = หลาย ops, หลาย backup, หลาย skill
→ เริ่ม 1-2 ตัว + เพิ่มเมื่อจำเป็น
Part 3: Compose Data Across Services
ปัญหา: หน้า detail ของ order ต้องการ user info + product info + payment info
3.1 Option A: API Composition
วิธีแรกที่ตรงไปตรงมาคือให้ gateway/BFF เรียกทุก service ที่เกี่ยวข้องตอน query แล้วประกอบผลรวมเอง — ได้ข้อมูล real-time ทันที แต่แลกมาด้วย 2 อย่าง: latency ที่สูงขึ้น (เพราะต้องรอหลาย sync call พร้อมกัน) และความเปราะ (service ใดล่ม query ทั้งก้อนก็พังตาม):
ข้อดี: ง่าย, real-time ข้อเสีย: 4 sync call = latency, ถ้า service ล่ม = fail
🎯 Decision tree เลือก option (ก่อนอ่าน B/C):
- Low volume / ต้องการ real-time (latency สำคัญ, traffic ไม่สูง) → A: API Composition
- High volume + ยอมรับ stale 1-3 วินาที (query เร็วเป็นหลัก) → B: Materialized View
- มี event-driven อยู่แล้ว + consumer ต้องการ snapshot ของ data → C: Event-Carried State Transfer
3.2 Option B: Materialized View (CQRS-like)
อีกวิธีคือสร้าง read model ที่ denormalize ไว้ล่วงหน้า — service หนึ่งฟัง event จากทุก service แล้วประกอบเป็นตารางเดียวพร้อม query ผลคือ query เร็วมาก (อ่านตารางเดียว) แต่ข้อมูลเป็น eventual consistent และต้องเขียน logic update ให้ครบ:
ข้อดี: query เร็ว 1 ตาราง ข้อเสีย: eventual consistency, complex update logic
3.3 Option C: Event-Carried State Transfer
Event มี data ที่ consumer ต้องการ:
json
{
"type": "OrderPlaced",
"orderId": "ord-123",
"user": {
"id": "usr-456",
"name": "Alice",
"email": "..."
},
"items": [
{ "productId": "p-1", "name": "Widget", "qty": 2, "price": 50 }
],
"total": 100
}Consumer ไม่ต้อง call กลับ — ใช้ data จาก event เลย
Part 4: CQRS — Command Query Responsibility Segregation
💡 Analogy: ห้องสมุดมหาวิทยาลัย
- Write side (เก็บ): บรรณารักษ์รับหนังสือ → จัด catalog → เก็บใน shelf ตาม Dewey Decimal (normalized, validated, ACID)
- Read side (อ่าน): ตู้ค้นหา + Google + Search engine ของห้องสมุด → user search "Java" → เห็นผลทันที (denormalized, indexed, fast)
ทำไมแยก:
- คนเก็บ (1 คน) + คนอ่าน (1000 คน) — ไม่ scale เท่ากัน
- คนเก็บต้อง strict + ตรวจ ISBN → ช้านิดหน่อยได้
- คนอ่านต้องเร็ว — query 50ms ไม่ทันใจ
- 1 หนังสือ → หลายมุมมอง (search by title, author, ISBN, subject, year)
4.1 หลักการ
ระบบทั่วไป (CRUD) ใช้ model เดียวกันสำหรับเขียน + อ่าน:
text
Client ──→ [Order Service] ──→ [DB: orders + order_items + customers]
│ │
└── Read ──────────┘ ← join 3 tables ทุก queryปัญหา: read หนัก = query slow + DB CPU พุ่ง → write affected
CQRS แยก write กับ read เป็น 2 path:
หลักการ 4 ข้อ:
- Write side optimize for correctness — strict validation, ACID, normalized
- Read side optimize for query speed — denormalized, indexed, multiple views
- Eventual consistency ระหว่าง 2 side — ยอมรับ stale read 1-3 วินาที
- Event-driven sync — write publish event, read consume + update
4.2 เหตุผลที่ใช้ — เมื่อไหร่คุ้ม
4 use cases ที่ CQRS คุ้มจริง:
Use case 1: Read-heavy (read:write ratio > 10:1)
text
E-commerce product page:
- Write: stock update 100 req/s
- Read: product view 10,000 req/s ← 100x
→ Read DB scale separately (read replica × 10)
→ Write DB ไม่กระทบUse case 2: Different shape ระหว่าง write/read
text
Write side (normalized — 1 order = 4 tables):
orders (id, customer_id, total)
order_items (order_id, product_id, qty)
customers (id, name, email)
products (id, name, price)
→ Create order = INSERT 4 tables in transaction (ACID)
Read side (denormalized — 1 row per order):
order_view (
order_id, customer_name, customer_email,
items: [{name, qty, price}], total, status
)
→ Query order = SELECT 1 row, no joinsUse case 3: Multiple views ของ 1 source
แต่ละ view optimize แยก — ไม่กระทบ write
Use case 4: Complex business validation บน write
text
Write side: complex DDD aggregate
Order.placeOrder() {
validate inventory available
validate customer credit
validate fraud check
validate pricing rules
apply discount logic
→ ใช้ rich domain model + DDD aggregate
}
Read side: simple projection
return readModelRepo.findById(orderId);→ Write side มี rich behavior, Read side เป็น dumb getter
4.3 Example
ตัวอย่างรูปธรรมของ CQRS — ฝั่ง write เก็บข้อมูลแบบ normalize (orders + order_items แยกตาราง) ส่วนฝั่ง read เก็บแบบ denormalize ยุบทุกอย่างไว้ตารางเดียว (order_view) พร้อม query ทันที สอง store นี้ optimize คนละเป้าหมาย:
sql
-- Write DB (Postgres)
CREATE TABLE orders (id, customer_id, total, status, created_at);
CREATE TABLE order_items (order_id, product_id, qty, price);
-- Read DB (denormalized, อาจ MongoDB หรือ Postgres ด้วย)
CREATE TABLE order_view (
order_id,
customer_name, ← denormalized
customer_email,
item_count,
total,
status,
created_at,
products JSONB ← embed
);4.4 Update Read Model
read model ไม่อัปเดตเอง — ต้องฟัง event จากฝั่ง write แล้วซิงค์ตาม เมื่อ order ถูกสร้างก็ insert, เมื่อชื่อลูกค้าเปลี่ยนก็ update field ที่ denormalize ไว้ นี่คือ logic ที่ทำให้ read model ตามทันความจริง (แต่เป็น eventual):
ใช้ event-driven:
java
@KafkaListener(topics = {"order.events", "customer.events", "product.events"})
public void updateReadModel(Event e) {
switch (e.type()) {
case "OrderPlaced":
readModelRepo.create(toView(e));
break;
case "CustomerNameChanged":
readModelRepo.updateCustomerName(e.customerId(), e.newName());
break;
case "ProductRenamed":
readModelRepo.updateProductName(e.productId(), e.newName());
break;
}
}4.5 ⚠️ CQRS Trade-offs
- Complexity — code 2 ทาง, update read model logic
- Eventual consistency — user อาจเห็น stale read
- Multiple data stores — เพิ่ม ops
อย่าใช้ CQRS ทุก service — ใช้เฉพาะ service ที่ read-heavy + complex query
Part 5: Event Sourcing (ลึก)
💡 Analogy: บัญชีธนาคาร
ระบบ banking ไม่ได้เก็บแค่ "balance = 120 บาท" — เก็บ ทุก transaction:
2026-01-01 เปิดบัญชี +0 2026-01-05 ฝากเงินเดือน +30,000 2026-01-10 จ่ายค่าเช่า -15,000 2026-01-15 ATM ถอน -2,000 2026-01-20 โอนให้แฟน -5,000 ───────────────────────────────────── Balance = sum = 8,000 บาท ← computed, ไม่ใช่ storedEvent Sourcing = pattern เดียวกัน — เก็บ event log, balance = sum
ทำไมทำแบบนี้?
- Audit trail — มีทุกอย่างที่เคยเกิด (ไม่มี history loss)
- ตอบคำถาม "ทำไม" — "ทำไม balance = 8,000?" → อ่าน log
- Replay เพื่อ debug — บั๊กตอน 15 มกรา? rewind ไปอ่านใหม่
- Time travel — balance ของวันที่ 10 = sum ของ events ก่อนวันที่ 10
Git ก็เป็น event sourcing — commit log = events, working tree = projection
5.1 หลักการ — เก็บ event, ไม่เก็บ state
ระบบทั่วไป (state-stored) เก็บ "ตอนนี้คือไง":
text
accounts table:
┌────────────┬─────────┐
│ account_id │ balance │
├────────────┼─────────┤
│ 1 │ 120 │ ← เก็บแค่ค่าปัจจุบัน
└────────────┴─────────┘
UPDATE: balance += 50 → balance = 170 ❌ ของเก่า (120) หายไปปัญหา: ทำไม balance = 170? ไม่รู้. มาจาก deposit หรือ refund หรือ transfer? ไม่รู้
Event Sourcing เก็บ "เกิดอะไรขึ้นบ้าง":
text
events table:
┌──────────┬────────────┬──────────────┬────────┬──────────────────────┐
│ event_id │ aggregate │ type │ amount │ timestamp │
├──────────┼────────────┼──────────────┼────────┼──────────────────────┤
│ 1 │ account-1 │ Opened │ 0 │ 2026-01-01T10:00:00 │
│ 2 │ account-1 │ Deposited │ 100 │ 2026-01-05T14:30:00 │
│ 3 │ account-1 │ Withdrawn │ 30 │ 2026-01-10T09:15:00 │
│ 4 │ account-1 │ Deposited │ 50 │ 2026-01-15T11:20:00 │
└──────────┴────────────┴──────────────┴────────┴──────────────────────┘
Current state = REPLAY events:
start: balance = 0
+ Opened(0) → 0
+ Deposited(100) → 100
+ Withdrawn(30) → 70
+ Deposited(50) → 120
─────────────────────────
Current balance = 120กฎ 4 ข้อของ Event Sourcing:
- Event = source of truth — state ทุกอย่างคำนวณจาก event
- Event immutable — append-only, ห้ามแก้หรือลบ event เก่า
- State = projection —
state = fold(events, initial_state) - Event = past tense fact —
OrderPlaced,MoneyDeposited(ไม่ใช่ command)
🎯 Event Sourcing ≠ CQRS (มัก confuse กัน)
| CQRS | Event Sourcing | |
|---|---|---|
| คือ | แยก write/read model | เก็บ event เป็น source of truth |
| ต้องใช้คู่ ES ไหม | ไม่ — CQRS ทำกับ state-stored ได้ | ไม่ — ES อ่านโดยตรงจาก projection ได้ |
| ส่วนมากใช้ | คู่กัน — ES generate event, CQRS projection อ่าน | คู่กับ CQRS |
| Complexity | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
Combo ที่พบบ่อย: ES + CQRS = "Write side stores events, Read side has projections" → ใช้ใน financial, audit, complex domain
💎 ข้อดี Event Sourcing
- Audit trail สมบูรณ์ — รู้ทุกอย่างที่เกิด (compliance: SOX, GDPR, HIPAA, banking)
- Time travel — query state ของ "ณ วันที่ 15 มกรา" = replay event ถึงวันนั้น
- Debug จาก production — replay event ใน dev → reproduce bug 100%
- Build read model ใหม่ทีหลังได้ — เพิ่ม view ใหม่ = replay event เก่า
- Event เป็น byproduct ฟรี — ไม่ต้อง implement Outbox แยก — event คือ source
⚠️ ข้อเสีย Event Sourcing
- Complex code — load aggregate = replay events ทุกครั้ง (ต้อง snapshot)
- Schema evolution ยาก — event เก่า 3 ปี ยังต้อง deserialize ได้
- Query state ปัจจุบันยาก — ต้องมี projection + ไม่ ad-hoc query ได้
- Storage โต — เก็บทุก event ตลอดกาล (compaction ทำได้แต่ซับซ้อน)
- เรียนรู้ยาก — paradigm ต่างจาก CRUD ที่ทีม familiar
🎯 เมื่อไหร่ใช้ Event Sourcing
✅ ใช้เมื่อ:
- Financial / accounting — บังคับมี audit trail (banking, payment, billing)
- Healthcare — patient record changes ต้องเก็บประวัติทั้งหมด
- Compliance-heavy — GDPR, SOX, HIPAA, audit log
- Domain มี history สำคัญ — booking system, inventory, supply chain
- Complex business logic + ต้อง debug จาก production ได้
❌ อย่าใช้เมื่อ:
- CRUD app ทั่วไป — overkill มหาศาล
- ทีมไม่เคยทำ event-driven — เรียนรู้ก่อน CQRS / Saga ดีกว่า
- Domain ที่ "current state คือทั้งหมด" — เช่น cache, session
💡 Pattern-based story (ไม่ใช่ public postmortem ที่เฉพาะ GitLab): ทีมที่ใช้ Event Sourcing โดยไม่มี snapshot และไม่มี retention policy (นโยบายกำหนดว่าจะเก็บข้อมูลไว้นานแค่ไหนก่อนลบ/ย้าย) จะเจอปัญหานี้:
- สาเหตุ: ไม่ snapshot + ไม่มี retention policy
- อาการ: storage โตเร็วมาก (เคสในวงการที่เคยเห็น: ~800 GB ใน 6 เดือน) → replay/projection ช้าลงเรื่อย ๆ
- แก้: snapshot ทุก N events (เช่น 100) + archive event ที่เก่ากว่า retention window ไปเก็บใน cold storage (ที่เก็บข้อมูลราคาถูกสำหรับข้อมูลที่ไม่ค่อยถูกอ่าน เช่น S3 Glacier)
(เคสจริงของ GitLab incident 2017 ที่ดังคือ DB wipe ของ production replica — ไม่ใช่เรื่อง Event Sourcing — ดูบท 18)
Mental model
→ 1 event log → N projection (CQRS pattern)
5.2 Code
ตัวอย่าง event sourcing ของ aggregate "Account" — ทุก action (deposit/withdraw) ไม่แก้ state ตรง ๆ แต่สร้าง event แล้ว apply เพื่อเปลี่ยน state พร้อมเก็บ event ไว้ส่วน load รีสร้าง state ปัจจุบันด้วยการ replay event ทั้งหมดตามลำดับ:
💡 record + sealed interface เป็น syntax ของ Java 17+ ที่ทำให้ประกาศ data class แบบ immutable (แก้ไขค่าไม่ได้หลังสร้าง) สั้นลง — ถ้าไม่คุ้นเคย ข้ามรายละเอียด syntax แล้วดูแค่ logic (
apply/when/load) ก็เข้าใจ pattern ได้
java
// record ประกาศ implements interface ได้อย่างเดียว — extends class ไม่ได้ (record implicit-extend java.lang.Record ไปแล้ว)
// ดังนั้น contract ร่วม (DomainEvent) ต้องเป็น sealed interface ไม่ใช่ abstract class
public sealed interface DomainEvent
permits AccountOpened, Deposit, Withdraw {
UUID eventId();
Instant occurredAt();
}
public record AccountOpened(UUID eventId, Instant occurredAt,
String accountId, double initial) implements DomainEvent {
public AccountOpened(String accountId, double initial) {
this(UUID.randomUUID(), Instant.now(), accountId, initial);
}
}
public record Deposit(UUID eventId, Instant occurredAt,
String accountId, double amount) implements DomainEvent {
public Deposit(String accountId, double amount) {
this(UUID.randomUUID(), Instant.now(), accountId, amount);
}
}
public record Withdraw(UUID eventId, Instant occurredAt,
String accountId, double amount) implements DomainEvent {
public Withdraw(String accountId, double amount) {
this(UUID.randomUUID(), Instant.now(), accountId, amount);
}
}
public class Account {
private String id;
private double balance;
private List<DomainEvent> uncommittedEvents = new ArrayList<>();
public static Account open(String id, double initial) {
Account a = new Account();
a.apply(new AccountOpened(id, initial));
return a;
}
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException();
apply(new Deposit(id, amount));
}
public void withdraw(double amount) {
if (balance - amount < 0) throw new InsufficientFundsException();
apply(new Withdraw(id, amount));
}
private void apply(DomainEvent event) {
when(event);
uncommittedEvents.add(event);
}
private void when(DomainEvent event) {
// sealed interface: ไม่ต้องมี default — compiler ตรวจ exhaustiveness ให้
// ถ้าเพิ่ม subtype ใหม่ใน permits แล้วลืมเพิ่ม case ที่นี่ → compile error ทันที
switch (event) {
case AccountOpened e -> {
this.id = e.accountId();
this.balance = e.initial();
}
case Deposit e -> this.balance += e.amount();
case Withdraw e -> this.balance -= e.amount();
}
}
public static Account load(List<DomainEvent> events) {
Account a = new Account();
events.forEach(a::when);
return a;
}
}5.3 Event Store
event store คือที่เก็บ event แบบ append-only — key คือ (aggregate_id, version) ที่บังคับลำดับและทำ optimistic concurrency (ถ้า version ที่คาดไว้ไม่ตรง = มีคนแก้ก่อน = conflict) โหลด state ด้วยการดึง event ทั้งหมดของ aggregate มาเรียงตาม version:
Schema:
sql
CREATE TABLE event_store (
aggregate_id UUID,
aggregate_type TEXT,
version INT,
event_type TEXT,
event_data JSONB,
metadata JSONB,
occurred_at TIMESTAMPTZ,
PRIMARY KEY (aggregate_id, version)
);Load:
sql
SELECT * FROM event_store WHERE aggregate_id = ? ORDER BY version;Save:
java
@Transactional
public void save(Aggregate aggregate, int expectedVersion) {
List<DomainEvent> events = aggregate.pullUncommittedEvents();
int v = expectedVersion;
for (DomainEvent e : events) {
eventStore.append(aggregate.id(), ++v, e);
}
}Optimistic concurrency — ถ้า expectedVersion ไม่ตรง = throw conflict
5.4 Snapshot
aggregate ที่มี event เป็นล้าน ๆ การ replay ใหม่ทุกครั้งจะช้ามาก — snapshot แก้โดยเก็บ state สำเร็จรูปทุก N event เวลาโหลดก็เริ่มจาก snapshot ล่าสุดแล้ว replay เฉพาะ event ที่เกิดหลังจากนั้น:
Replay 1M event ช้า → snapshot ทุก N events:
text
event 1
event 2
...
event 1000
SNAPSHOT (state @ 1000)
event 1001
...
Load:
load latest snapshot (v=1000)
replay events 1001 onwards5.5 Projection (Read Model)
event store ตอบคำถาม "เกิดอะไรขึ้นบ้าง" ได้ดีแต่ query ปัจจุบันช้า — projection คือการสร้าง read model (เช่นตาราง balance) จากการฟัง event stream แล้วอัปเดตตาม ทำให้ query สถานะปัจจุบันได้เร็วเหมือน DB ปกติ:
Build read model จาก event stream:
java
@KafkaListener(topics = "account-events")
public void onAccountEvent(DomainEvent e) {
switch (e) {
case AccountOpened ev:
jdbc.update("INSERT INTO account_balance VALUES (?, ?)",
ev.accountId(), ev.initial());
break;
case Deposit ev:
jdbc.update("UPDATE account_balance SET balance = balance + ? WHERE id = ?",
ev.amount(), ev.accountId());
break;
case Withdraw ev:
jdbc.update("UPDATE account_balance SET balance = balance - ? WHERE id = ?",
ev.amount(), ev.accountId());
break;
}
}5.6 ข้อดี
- Audit log built-in — ทุก action เก็บ
- Time travel — replay ถึงจุดใดก็ได้
- Multiple views — projection เดียวกัน, view ต่างกัน
- Debug — replay เพื่อหา bug
- GDPR — สามารถ apply transformation (anonymize) ตอน replay
5.7 ข้อเสีย
แต่ event sourcing แลกมาด้วยความซับซ้อนที่ต้องยอมรับ — schema ของ event ต้อง version, storage โตเรื่อย ๆ, query รายงานช้าเพราะต้อง project ก่อน และต้องบริหาร snapshot ชั่งให้ดีว่าคุ้มกับประโยชน์ไหม:
- Complexity — schema evolution ของ event
- Storage growing — events เก็บตลอด
- Reporting query slow — ต้อง project ก่อน
- Snapshot management
- Schema versioning — event v1 vs v2
5.8 ⚠️ อย่าใช้ Event Sourcing ทุกที่
Use เมื่อ:
- Domain ต้องการ audit (financial, healthcare)
- Replay เป็น requirement
- Multiple read model จากเหตุการณ์เดียวกัน
- Complex business event (insurance, supply chain)
ไม่ใช้เมื่อ:
- Simple CRUD
- Team ไม่เคยทำ ES
- Schema เปลี่ยนบ่อยมาก
Part 6: Tools
6.1 Axon Framework (Java/Spring)
Axon เป็น framework ที่ทำ CQRS + event sourcing ให้ใน Java/Spring แบบครบวงจร — แค่ใส่ annotation (@Aggregate, @CommandHandler, @EventSourcingHandler) framework จะจัดการ command bus, event store และการ replay ให้ ไม่ต้องเขียน infra เอง:
java
@Aggregate
public class Order {
@AggregateIdentifier
private String id;
@CommandHandler
public Order(CreateOrderCommand cmd) {
AggregateLifecycle.apply(new OrderCreatedEvent(cmd.id(), cmd.amount()));
}
@EventSourcingHandler
public void on(OrderCreatedEvent e) {
this.id = e.id();
}
}Axon Server เป็น event store + command bus
6.2 EventStoreDB
EventStoreDB เป็นฐานข้อมูลที่ออกแบบมาเพื่อ event sourcing โดยเฉพาะ — มี stream subscription (ฟัง event ใหม่ ๆ), projection ในตัว และ client หลายภาษา เหมาะเมื่ออยากได้ event store เต็มรูปแบบโดยไม่ต้องประกอบเองจาก Kafka/DB:
Purpose-built event store:
- Stream subscriptions
- Projections
- Multi-language client
6.3 Kafka + Custom
ถ้ามี Kafka อยู่แล้วก็ใช้เป็น event store ได้ — 1 topic ต่อ aggregate type (partition ตาม id เพื่อรักษา ordering), เปิด compaction + retention ยาว (หรือ tier ไป S3) แล้วเก็บ state ไว้ใน projection service เอง แต่ต้องเขียน logic เพิ่มเองมากกว่า tool สำเร็จ:
Topic per aggregate type:
text
account-events (partition by accountId)
order-eventsCompaction + retention forever (หรือ tier S3)
State ใน projection service
Part 7: Reporting + Analytics
7.1 ปัญหา
reporting/analytics ในโลก microservices ยากเพราะข้อมูลกระจายอยู่หลาย service — คำถามอย่าง "ยอดขายต่อภูมิภาคต่อเดือน" ต้อง aggregate ข้ามหลาย DB ที่ไม่ได้ออกแบบให้ join กัน ต้องมีกลยุทธ์รวมข้อมูลโดยไม่กระทบ production:
หา "total sales per region per month" จาก microservices = ต้อง aggregate ข้าม service
7.2 Solutions
Option A: Per-service reporting
แต่ละ service expose /reports/... endpoint → aggregator call
Option B: Data Lake / Warehouse
→ Analytics ไม่กระทบ production DB
Option C: CDC + ETL
text
Service DB ─Debezium─→ Kafka ─Connect─→ WarehouseOption D: HTAP (Hybrid Transactional/Analytical Processing)
📖 HTAP (ไฮ-แทป = Hybrid Transactional/Analytical Processing) = DB ที่รองรับทั้งการเขียน/อ่านแบบ real-time transaction (OLTP) และการ query วิเคราะห์ข้อมูล (OLAP) ในเวลาเดียวกัน — ไม่ต้องแยก DB สองชุด
ใช้ DB ที่ทำ OLTP + OLAP ได้ (TiDB, SingleStore)
→ ใช้สำหรับ analytics ไม่ต้องตามใจ scale แยก
Part 8: Data Mesh
8.1 หลักการ
Data Mesh คือแนวคิดกระจายความรับผิดชอบเรื่องข้อมูล — แทนที่จะมี central data team เป็นคอขวด ให้แต่ละ domain team เป็นเจ้าของข้อมูลตัวเองและ publish เป็น "data product" ที่มี documentation + SLA ให้ทีมอื่นใช้ โดยมี governance กลางคุมมาตรฐาน:
แทนที่ "central data team" — แต่ละ domain team own ข้อมูลเป็น product:
text
Each domain team:
- Own raw data
- Publish "Data Product" (curated, documented, SLA)
- Other team consume Data Product
Federated governance:
- Catalog ทุก data product
- Common schema convention
- Quality SLA8.2 Building Block
Data Mesh ยืนบน 3 เสาหลัก — Data Product (ข้อมูลที่ documented/versioned/มี SLA), self-serve platform (เครื่องมือกลางที่ทุกทีมใช้สร้าง/ใช้ data product เองได้), และ federated governance (กฎกลาง + ปฏิบัติแบบกระจาย) ขาดอันใดอันหนึ่งก็ไม่เวิร์ก:
- Data Product = dataset ที่ documented + versioned + SLA
- Self-serve platform = tool ที่ทุกคนใช้ได้ (Kafka, S3, etc)
- Federated governance — central rule + decentralized exec
8.3 ใช้เมื่อ
Data Mesh คุ้มเฉพาะองค์กรใหญ่ที่ central data team กลายเป็นคอขวดจริง ๆ — ถ้าทีมเล็กหรือมี domain เดียวจะ over-engineer เปล่า ๆ ประเมินขนาดและความเจ็บปวดก่อนตัดสินใจ:
- Organization size > 500 dev
- Multiple business domain
- Central data team becomes bottleneck
Part 9: Data Replication Patterns
9.1 Cache (read-only replica)
เพื่อหลีกเลี่ยงการเรียกข้าม service ทุก request เราให้ service B เก็บสำเนา (cache) ของข้อมูลที่ service A เป็นเจ้าของ — A publish event เมื่อข้อมูลเปลี่ยน B อัปเดต cache ตาม ข้อมูลอาจ stale บ้างแต่ยอมรับได้กับข้อมูลที่เปลี่ยนไม่บ่อย (profile, catalog, pricing):
Service A own data. Service B cache:
text
[A DB] → A publishes events → [B local cache]
B reads cache (fast, eventual)Service B's cache stale OK เพราะ:
- User profile cache (1 hour OK)
- Product catalog
- Pricing table
9.2 Reference Data Replication
ทุก service ที่ใช้ "country code" — replicate table นั้นเข้า service local
→ ไม่ต้อง call CountryService ทุก request
9.3 ⚠️ อย่า replicate ทุกอย่าง
การ replicate มีต้นทุน (sync logic, storage, ความซับซ้อน) จึงไม่ควรทำกับทุกอย่าง — เลือกเฉพาะข้อมูลที่ "access บ่อยแต่เปลี่ยนน้อย" เท่านั้น ส่วนข้อมูลที่เปลี่ยนบ่อยหรือใช้นาน ๆ ครั้งให้เรียกตรงดีกว่า:
แค่ data ที่ access บ่อย + change น้อย
Part 10: Migration Strategy
10.1 Strangler with Database
การแยก DB ออกจาก monolith ทำทีเดียวไม่ได้ — ใช้ Strangler pattern ค่อย ๆ ดึงข้อมูลของ service ออกมา: ตั้ง DB ใหม่ + sync สองทาง (CDC) กับ monolith, ปรับ monolith ให้อ่านผ่าน service ใหม่ทีละส่วน จนสุดท้าย service เป็นเจ้าของข้อมูลเต็มตัว:
10.2 Branch-by-abstraction
1. Repository interface
2. Implementation A (old DB) + Implementation B (new DB) + sync
3. Toggle via feature flag
4. Verify B
5. Remove APart 10.5: Dual-Write Trap (กับดักที่ทุก microservices เจอ)
ปัญหา: write DB และ publish event = 2 system → ไม่มี atomic guarantee
java
// ❌ NEVER do this
@Transactional
public Order placeOrder(Cmd cmd) {
Order o = orderRepo.save(...); // DB commit (success)
kafka.send("orders", event).get(); // Kafka publish (could fail!)
// Outcomes:
// 1. ทั้งคู่สำเร็จ — OK
// 2. DB OK, Kafka fail — event หาย (downstream ไม่รู้)
// 3. DB OK, Kafka network blip, retry → duplicate
// 4. Kafka OK, DB rollback — event "ผี" (event เกิดแต่ไม่มีจริง)
return o;
}10.5.1 4 anti-pattern ที่ลองแล้วไม่สำเร็จ
| ลอง | ปัญหา |
|---|---|
| 2PC (XA transaction) | technically รองรับ (Postgres PREPARE TRANSACTION, MS DTC, JTA/XA) แต่ industry เลี่ยง: lock contention, coordinator availability, recovery ซับซ้อนเมื่อ coordinator ล่มกลาง prepare |
| Try-publish-then-save | event ผี ถ้า save fail |
| Try-save-then-publish | event หาย ถ้า publish fail |
| Compensate-on-publish-fail | compensation อาจ fail ด้วย → loop |
10.5.2 Solution: Outbox Pattern (ทำเสมอ)
ทางออกเดียวที่ใช้ได้จริงคือ Outbox — เขียน order + event ลง DB ใน transaction เดียว (atomic) แล้วให้ Debezium อ่าน outbox จาก WAL ไป publish Kafka ภายหลัง ตัดปัญหา dual write ออกหมดเพราะ service เขียนแค่ DB ที่เดียว:
java
@Transactional
public Order placeOrder(Cmd cmd) {
Order o = orderRepo.save(...);
outboxRepo.save(new OutboxEvent("OrderPlaced", o.id(), toJson(o)));
return o;
// 1 DB transaction → atomic guarantee
}
// Separate process (Debezium CDC):
// Postgres WAL → Debezium → Kafka — guaranteed at-least-once→ See บท 12 Part 3 สำหรับ Outbox pattern ละเอียด + Debezium SMT
10.5.3 PostgreSQL Logical Replication + Debezium 3.x (2026 setup)
sql
-- 1. Enable on Postgres — ⚠️ 3 ใน 4 settings นี้เป็น postmaster params (ต้อง restart)
ALTER SYSTEM SET wal_level = logical;
ALTER SYSTEM SET max_wal_senders = 10;
ALTER SYSTEM SET max_replication_slots = 10;
ALTER SYSTEM SET wal_sender_timeout = '60s'; -- ปล่อย sender ถ้า client เงียบ (setting นี้ reload ได้ ไม่ต้อง restart)
-- 🚨 ต้อง RESTART Postgres ก่อนทำขั้นต่อ (สำหรับ wal_level, max_wal_senders, max_replication_slots) — pg_reload_conf() / SIGHUP ไม่พอสำหรับ 3 ตัวนี้
-- wal_sender_timeout ต่างจาก 3 ตัวข้างต้น — ใช้ pg_reload_conf() / SIGHUP ได้เลยไม่ต้อง restart
-- ถ้าข้าม restart: ขั้น 2-4 อาจสำเร็จ แต่ Debezium จะ start fail ด้วย "wal_level not logical"
-- ตรวจหลัง restart: SHOW wal_level; → ต้องเป็น 'logical'
-- 2. Create publication (only tables you want CDC)
CREATE PUBLICATION dbz_pub FOR TABLE orders, outbox WITH (publish = 'insert,update,delete');
-- 3. Replication slot (Debezium creates this)
SELECT pg_create_logical_replication_slot('debezium', 'pgoutput');
-- 4. Replica identity (for UPDATE/DELETE events to include "before")
ALTER TABLE orders REPLICA IDENTITY FULL;🚨 อย่าลืม restart Postgres ก่อนสร้าง slot — เป็น mistake ที่เจอบ่อยที่สุดเมื่อ setup CDC ครั้งแรก. Monitor slot lag เป็นนิสัย:
sqlSELECT slot_name, active, confirmed_flush_lsn, pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn) AS lag_bytes FROM pg_replication_slots;
Debezium 3.x config (2.7+ ก็ใช้ syntax นี้):
json
{
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"plugin.name": "pgoutput",
"database.hostname": "postgres",
"database.port": "5432",
"publication.name": "dbz_pub",
"slot.name": "debezium",
"topic.prefix": "shop",
"table.include.list": "public.outbox,public.orders",
"transforms": "outbox",
"transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
"transforms.outbox.route.by.field": "aggregate_type",
"transforms.outbox.route.topic.replacement": "${routedByValue}.events",
"schema.history.internal.kafka.bootstrap.servers": "kafka:9092",
"schema.history.internal.kafka.topic": "shop.schema-history",
"signal.data.collection": "public.debezium_signal"
}💡
signal.data.collection(Debezium 2.4+) — trigger snapshot ระหว่างรันด้วย INSERT row → reprocess subset ของ data ได้โดยไม่ restart connector
Part 10.6: Zero-Downtime Data Migration (Expand-Contract)
ปัญหา: เปลี่ยน schema ใน production ที่มี user 24/7 → break old version ระหว่าง rolling
10.6.1 4-step Expand-Contract
text
Stage 0: table users { id, name }
Service v1 reads/writes name
Stage 1 (Expand):
ALTER TABLE users ADD COLUMN full_name TEXT
Backfill: UPDATE users SET full_name = name
Service v1 ยังใช้ name ตามเดิม (ไม่กระทบ)
Stage 2 (Dual write):
Deploy Service v2 — writes both `name` AND `full_name`
Service v1 + v2 รันพร้อมกัน (rolling) — ทั้งคู่ทำงานได้
Stage 3 (Migrate read):
Deploy Service v3 — reads `full_name`, writes both
Old v2 ค่อย ๆ ถูก replace
Stage 4 (Contract):
ALTER TABLE users DROP COLUMN name
Service v3 → v4 (writes only full_name)ทั้งกระบวนการอาจใช้เวลา 1-4 สัปดาห์ — อย่ารีบ เพราะ rollback ต้องทำได้ทุก stage
10.6.2 Online schema change tools
ALTER TABLE บนตารางใหญ่ (>10M rows) จะ lock ตารางและช้าจนกระทบ production — มีเครื่องมือ online schema change ที่ทำงานโดยไม่ lock โดยสร้าง shadow table แล้ว backfill + swap ทีหลัง เลือกตาม DB ที่ใช้:
ตอน ALTER TABLE บน table ใหญ่ (>10M rows) จะ lock + slow:
| Tool | DB |
|---|---|
| pg_repack | Postgres |
| pg-online-schema-change | Postgres |
| gh-ost (GitHub) | MySQL |
| pt-online-schema-change (Percona) | MySQL |
| Vitess online DDL | MySQL at scale |
Pattern: สร้าง shadow table → backfill → swap atomically → drop old
10.6.3 Database-per-service migration (extract from monolith)
text
Step 1: Service A อ่าน monolith DB ตามเดิม
Step 2: สร้าง Service A's own DB + CDC sync ทั้ง 2 ทาง
Step 3: Service A write to A's DB, sync back (read still monolith)
Step 4: Service A read from A's DB (sync back stopped)
Step 5: Monolith ลบ table A's domain→ See บท 01 Part 7 สำหรับ Strangler Fig + database split detail
Part 10.7: Production War Stories
🔥 Square (2019): dual write ทำ payment hang 30 ชั่วโมง
Charge service write DB + send Kafka event → Kafka cluster fail → exception → DB rollback → user เห็น "payment fail" แต่ acquirer (Visa) charge แล้ว
Lesson:
- Outbox + CDC → ไม่มี cross-system transaction
- ถ้า must dual-write → at least retry forever กับ idempotency
- Audit log + reconciliation job ทุก 1 ชม.
🔥 Case Study: Event Sourcing storage growth ทะลุ 800GB ใน 6 เดือน (composite pattern)
⚠️ หมายเหตุ: นี่คือเคสสมมติที่สร้างจาก pattern ที่พบในวงการ — ไม่ใช่ postmortem จริงของ GitLab (GitLab incident 2017 ที่ดังคือ production DB wipe ไม่เกี่ยวกับ Event Sourcing — ดูบท 18)
Event store ไม่มี snapshot → query "current state" ของ aggregate = replay 100k events → slow + storage โต
Lesson:
- Snapshot ทุก 100-1000 events
- Compact stream เมื่อจบ aggregate lifecycle
- Cold storage (S3 + tiered) สำหรับ event เก่า
- Consider: event sourcing ไม่ใช่ default — เลือก aggregate ที่ต้องการ audit จริง ๆ
🔥 ทีม fintech (2024): Debezium WAL slot bug ทำ Postgres เต็ม
Debezium snapshot ใหม่ระหว่าง replication slot active → slot LSN ค้าง → WAL เก็บไว้ 14 วัน → 2TB → DB freeze
Lesson:
- Monitor
pg_replication_slots.confirmed_flush_lsnlag - Alert ถ้า WAL retention > threshold
- ใช้
signal.data.collection(Debezium 2.4+) แทน connector restart - Backup ของ WAL config — เคลียร์ slot ที่ stale
🔥 บริษัท health tech (2023): expand-contract ไม่ทำ → migration ทำระบบล่ม 4 ชม.
ALTER TABLE rename column ใน 1 deploy → v1 pods ที่ rolling ยังใช้ชื่อเก่า → SELECT fail → 500 ทั้งระบบ
Lesson:
- Schema change ต้อง backward compat อย่างน้อย 2 version
- Expand → Dual-write → Migrate read → Contract — เว้นแต่ละ stage 1-2 สัปดาห์
- Test rolling deployment with mixed versions ก่อน prod
Part 10.8: 📋 Cheat Sheet
Pattern selection
DB-per-service checklist
text
☐ ทุก service มี schema ของตัวเอง (separate logical DB)
☐ ห้ามให้ service อื่น query DB ตรง ๆ
☐ Cross-service data → API หรือ Event เท่านั้น
☐ Schema migration tool (Flyway/Liquibase) ใน CI
☐ Backup policy per service
☐ Connection pool sized correctly (per pod × replica < DB max_conn)
☐ Read replica สำหรับ heavy read
☐ Monitoring per DB (separate alert)Migration deployment order
text
1. Migration (additive only — ADD COLUMN, ADD INDEX CONCURRENTLY)
2. Deploy app v2 (handles both old + new schema)
3. Backfill data (background job, chunked, throttled)
4. Verify data integrity
5. Deploy app v3 (uses only new schema)
6. Migration (cleanup — DROP COLUMN, etc.)
⚠️ Migration ที่ "remove" จะ run หลัง deploy เสมอ (never before)Reporting architecture (modern 2026)
📖 กางศัพท์ของ stack data analytics สมัยใหม่:
- Data Lake = ที่เก็บไฟล์ดิบ (parquet, csv, json) บน object storage (S3) — ถูก, scale ใหญ่มาก แต่ไม่มี transaction
- Data Warehouse = DB optimized สำหรับ analytics (BigQuery, Snowflake, Redshift) — แพง แต่มี SQL + ACID
- Data Lakehouse = "ลูกผสม" — ไฟล์อยู่บน S3 (ถูก) แต่มี metadata layer ที่ให้ ACID + time-travel + schema evolution เหมือน DB
- Apache Iceberg / Delta Lake / Apache Hudi = 3 format ของ lakehouse metadata — Iceberg เป็น Apache project ที่ vendor-neutral ที่สุด (Snowflake, BigQuery, AWS Athena, Trino รองรับ), Delta Lake = Databricks origin, Hudi = Uber origin
- Trino = SQL engine ที่ query ข้าม source หลายตัว (S3 + DB + Kafka) — ไม่เก็บ data เอง (ชื่อเดิมคือ PrestoSQL — เปลี่ยนชื่อปี 2021)
Part 11: Anti-patterns
11.1 Shared Database
anti-pattern ที่ร้ายแรงที่สุด — หลาย service ใช้ DB เดียวกันทำให้แก้ schema ทีกระทบทุก service และ couple กันแน่นจนไม่ต่างจาก monolith นี่ไม่ใช่ microservices จริง:
ใหญ่สุด — ไม่ใช่ microservices
11.2 Cross-service JOIN ผ่าน DB link
การ JOIN ข้าม service ผ่าน DB link ดูเหมือนเลี่ยง shared DB ได้ แต่จริง ๆ คือ shared DB ในคราบใหม่ — ยัง couple schema กันแน่นเหมือนเดิม ควรใช้ API composition หรือ event แทน:
SQL through DB link ไป service อื่น = same as shared DB
11.3 Distributed Transaction (2PC, XA)
2PC/XA ดึงทุก service เข้า transaction เดียวกัน ดูปลอดภัยแต่ช้า, มี single point of failure ที่ coordinator และ vendor lock — ใน microservices ให้ใช้ Saga (eventual consistency) แทน:
Don't! Use Saga
11.4 Synchronous Composition Always
ใช้ sync composition กับทุก query (เรียก 4 service ต่อ request) ทำให้ latency บวกสะสมและเปราะ (service ใดช้า/ล่มก็พังทั้ง request) — งาน read-heavy ควรใช้ materialized view แทน:
4-way fan-out ทุก request → latency cascade
11.5 Event Sourcing on Everything
event sourcing ทรงพลังแต่ซับซ้อน การใช้กับทุก service รวมถึง CRUD ธรรมดาคือ over-engineering ที่เพิ่มภาระโดยไม่ได้ประโยชน์ — ใช้เฉพาะ domain ที่ต้องการ audit/replay จริง:
Over-engineering
11.6 No Schema Registry
Event break consumer
Part 12: Checkpoint
- ทำไม shared DB เป็น anti-pattern?
- Polyglot persistence ดียังไง? ระวังอะไร?
- API Composition vs Materialized View?
- CQRS แยก write/read เพราะอะไร?
- Event Sourcing เก็บอะไร? state คือ?
- Snapshot ใน ES ใช้แก้อะไร?
- ใช้ ES เมื่อไหร่? ไม่ใช้เมื่อไหร่?
- Data Mesh คืออะไร?
- CDC + Data Warehouse pattern?
- Reference data replication ทำอะไร?
Part 13: สรุปบทนี้
- DB per service = บังคับ ใน microservices
- Polyglot persistence = เลือก DB ตาม use case (Postgres, Mongo, Redis, Elastic, ClickHouse, pgvector)
- API Composition หรือ Materialized View สำหรับ cross-service query
- CQRS = แยก write/read model — ใช้เมื่อจำเป็น
- Event Sourcing = เก็บ events → projection. Audit + replay + snapshot
- 🆕 Dual-write trap = ทำผิดเกือบทุกคน — ใช้ Outbox + CDC เสมอ
- 🆕 Postgres Logical Replication + Debezium 3.x = standard CDC pipeline (2.x ยัง maintained; 3.x แนะนำสำหรับ greenfield)
- 🆕 Expand-Contract = zero-downtime schema migration (4 stage, 2-4 wk)
- 🆕 Online schema change (pg_repack, gh-ost, Vitess) สำหรับ table ใหญ่
- Data Mesh = scale data ownership ใน org > 500 dev
- CDC + Lakehouse (Iceberg/Delta) + Trino/ClickHouse = reporting modern stack
- Saga > Distributed Transaction (2PC) เสมอ
บทถัดไป — Distributed Tracing + Correlation