Skip to content

บทที่ 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 caseDB choice
Transactional CRUDPostgreSQL, MySQL
Document / flexible schemaMongoDB
Time-seriesTimescaleDB, InfluxDB
Graph relationshipNeo4j, Amazon Neptune
SearchElasticsearch, OpenSearch, Meilisearch
CacheRedis
Wide-column / big dataCassandra, 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 / analyticsClickHouse, BigQuery, Snowflake
Vector (AI/ML)Pinecone, pgvector, Weaviate
Event storeEventStoreDB, Kafka
ObjectS3, 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 ของ dataC: 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 ข้อ:

  1. Write side optimize for correctness — strict validation, ACID, normalized
  2. Read side optimize for query speed — denormalized, indexed, multiple views
  3. Eventual consistency ระหว่าง 2 side — ยอมรับ stale read 1-3 วินาที
  4. 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 joins

Use 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, ไม่ใช่ stored

Event 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:

  1. Event = source of truth — state ทุกอย่างคำนวณจาก event
  2. Event immutable — append-only, ห้ามแก้หรือลบ event เก่า
  3. State = projectionstate = fold(events, initial_state)
  4. Event = past tense factOrderPlaced, MoneyDeposited (ไม่ใช่ command)

🎯 Event Sourcing ≠ CQRS (มัก confuse กัน)

CQRSEvent 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

  1. Audit trail สมบูรณ์ — รู้ทุกอย่างที่เกิด (compliance: SOX, GDPR, HIPAA, banking)
  2. Time travel — query state ของ "ณ วันที่ 15 มกรา" = replay event ถึงวันนั้น
  3. Debug จาก production — replay event ใน dev → reproduce bug 100%
  4. Build read model ใหม่ทีหลังได้ — เพิ่ม view ใหม่ = replay event เก่า
  5. Event เป็น byproduct ฟรี — ไม่ต้อง implement Outbox แยก — event คือ source

⚠️ ข้อเสีย Event Sourcing

  1. Complex code — load aggregate = replay events ทุกครั้ง (ต้อง snapshot)
  2. Schema evolution ยาก — event เก่า 3 ปี ยังต้อง deserialize ได้
  3. Query state ปัจจุบันยาก — ต้องมี projection + ไม่ ad-hoc query ได้
  4. Storage โต — เก็บทุก event ตลอดกาล (compaction ทำได้แต่ซับซ้อน)
  5. เรียนรู้ยาก — 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 onwards

5.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-events

Compaction + 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─→ Warehouse

Option 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 SLA

8.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 A

Part 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-saveevent ผี ถ้า save fail
Try-save-then-publishevent หาย ถ้า publish fail
Compensate-on-publish-failcompensation อาจ 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 เป็นนิสัย:

sql
SELECT 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:

ToolDB
pg_repackPostgres
pg-online-schema-changePostgres
gh-ost (GitHub)MySQL
pt-online-schema-change (Percona)MySQL
Vitess online DDLMySQL 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_lsn lag
  • 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

การ 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

  1. ทำไม shared DB เป็น anti-pattern?
  2. Polyglot persistence ดียังไง? ระวังอะไร?
  3. API Composition vs Materialized View?
  4. CQRS แยก write/read เพราะอะไร?
  5. Event Sourcing เก็บอะไร? state คือ?
  6. Snapshot ใน ES ใช้แก้อะไร?
  7. ใช้ ES เมื่อไหร่? ไม่ใช้เมื่อไหร่?
  8. Data Mesh คืออะไร?
  9. CDC + Data Warehouse pattern?
  10. 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


← บทที่ 13 | สารบัญ | บทที่ 15: Tracing + Correlation →