บทที่ 18 · Part 4 — Boundaries, Data and Integration

Persistence and Data Boundaries

แยก domain, persistence, write/read model และ cache โดยใช้ MySQL กับ Redis ตาม authoritative responsibility

Persistence and Data Boundaries

ระบบแสดง Wallet balance จาก Redis ได้เร็ว ทีมจึงนำค่าเดียวกันไปอนุมัติ Transfer ช่วง cache lag ผู้ใช้โอนเกิน available balance อีกด้านหนึ่ง Repository คืน ORM model ที่มีทุก column จน domain, API และ cache ใช้ type เดียวกัน Data architecture ต้องบอก ความหมายและ authority ของแต่ละ representation ก่อนเลือก datastore

จบบทนี้คุณจะ

แยก domain, persistence, write, read model และ cache วาง Repository/Data Mapper เมื่อ ปกป้อง complexity จริง ออกแบบ MySQL transaction/concurrency กับ Redis derived state และสร้าง data responsibility map สำหรับ financial decision path

[!IMPORTANT] วิธีอ่านหลักฐานในบทนี้ คำที่เป็น Definition/Pattern มีแหล่งต้นทางอยู่ใกล้ claim ส่วน decision table, starting structure, checklist และ lab เป็น Heuristic/Trade-off หรือ Course Convention สำหรับฝึกตัดสินใจ ไม่ใช่มาตรฐานสากล เว้นแต่บทจะระบุแหล่งและขอบเขตไว้ชัดเจน

Model 5 ชนิด

ModelPurposeOwner
Domain modelตัดสิน rule/invariantowning business module
Persistence modelmap schema/row/versionpersistence adapter
Write modelcommand/state สำหรับ authoritative changeapplication/domain boundary
Read modelquery shape ที่ optimize การอ่านquery owner
Cacheสำเนา derived ที่มี freshness/evictioncache adapter/consumer

type อาจเหมือนกันใน module เล็กได้ แต่ต้องเป็น deliberate choice อย่า share เพราะขี้เกียจ map Field ที่อ่านเพื่อ display ไม่ควรเปิด method เปลี่ยน domain state

MySQL ในบทบาท Authoritative Store

MySQL เหมาะเป็นตัวอย่าง relational authority ด้วย transaction, constraints และ locking แต่ต้องออกแบบตาม schema/engine/version จริง:

  • primary/unique key สำหรับ identity/idempotency
  • foreign/check constraints เท่าที่ invariant อยู่ใน row/relation ที่ database enforce ได้
  • BEGIN/COMMIT/ROLLBACK สำหรับ atomic local state change
  • optimistic version หรือ locking read ตาม contention/failure model
  • tenant ID ใน key/query และ DB grants/schema separation ตาม risk
  • migration/backfill plan ที่ preserve old/new application compatibility

ตัวอย่าง idempotent credit:

CREATE TABLE wallet_credits (
  credit_id VARCHAR(40) PRIMARY KEY,
  wallet_id VARCHAR(40) NOT NULL,
  operation_id VARCHAR(80) NOT NULL,
  amount_minor BIGINT NOT NULL,
  currency CHAR(3) NOT NULL,
  UNIQUE KEY uq_wallet_operation (wallet_id, operation_id),
  CHECK (amount_minor > 0)
);

จำนวนเงินจริงใช้ integer minor units แต่ currency metadata/precision policy ต้องยืนยัน Unique key ทำให้ retry logical operation ไม่ insert effect ซ้ำ ส่วน application map duplicate เป็น outcome เดิมหรือ conflict ตาม command contract

ขอบเขตของ Transaction และ Concurrency

Go transaction guide แนะนำใช้ sql.Tx และหลีกเลี่ยงการเรียก non-transaction sql.DB ปนใน flow เดียว MySQL locking reads มี semantics ตาม isolation/index/query shape จึงต้อง test กับ engine/version จริง

เลือก:

  • Optimistic version เมื่อ conflict ไม่บ่อยและ retry/reload ชัด
  • Pessimistic lock เมื่อ serialization จำเป็นและ transaction สั้น
  • Unique/check constraint สำหรับ invariant ที่ database แสดงได้
  • Ledger/accounting design เฉพาะเมื่อมี authoritative requirements ไม่ใช่เพราะ DDD

Domain invariant ต้องมี storage-level partner

Method ApproveRefund ป้องกัน rule ใน memory แต่ concurrent transactions อาจอ่าน version เดียวกัน ต้องใช้ version/lock/constraint และ concurrent integration test ห้ามถือ Aggregate เป็น concurrency mechanism ด้วยตัวเอง

Redis ในบทบาท Derived State ที่สร้างใหม่ได้

Redis cache-aside ให้ application อ่าน cache แล้ว fallback source-of-truth ก่อน populate Redis ในคอร์สนี้ Redis เป็นตัวอย่าง derived/replaceable state:

Key: portal:wallet-summary:{tenant}:{wallet}
Value: posted/available display fields + sourceVersion + generatedAt
TTL: chosen from freshness requirement
Source: Wallet query contract
Rebuild: query owner and repopulate

ต้องออกแบบ cache miss, stale read, invalidation, stampede, key namespace, tenant isolation, serialization version และ outage behavior ถ้า Redis ล่ม decision path ไม่ควร fail-open ด้วย stale value

Redis ทำ authoritative state ได้ในบางระบบ แต่ต้องพิสูจน์ durability/consistency/operations ตาม design นั้น ไม่ใช่ claim ของคอร์สนี้ Default teaching stance ทำให้ cache delete/rebuild ได้

แยก Decision Path จาก Display Path

Portal แสดงข้อมูลพร้อม asOf/freshness ได้ แต่ command ต้องส่ง Wallet ID/amount ไม่ใช่ “balance ที่ browser เห็น” เพื่อให้ owner re-read และตรวจ invariant

แบบฝึกปฏิบัติ: Data Responsibility Map

กรอกต่อ data set:

Name:
Owner module/context:
Authoritative source:
Write path and transaction:
Concurrency rule:
Read/display copies:
Freshness and invalidation:
Rebuild procedure:
Allowed decision uses:
Tenant/security controls:
Tests and operator evidence:

ทำสำหรับ Charge, Refund, Wallet Summary, Invoice View และ idempotency record ทดสอบ cache outage, concurrent refund และ retry credit operation

รายการตรวจสอบ

  • Domain/persistence/read/cache models มี purpose แยก
  • Authoritative source และ owner ระบุชัด
  • MySQL constraints/transaction/concurrency match invariant
  • Tenant scope enforce ใน query/key/test
  • Redis state มี TTL/version/rebuild/outage behavior
  • Display state ไม่เข้า financial decision โดยเงียบ ๆ
  • Repository ปกป้อง complexity แทนการซ่อน datastore capability ทั้งหมด

สรุปบทนี้

Data boundary ไม่ใช่การห่อ SQL ด้วย Repository อย่างเดียว ต้องแยก authority, purpose, freshness และ concurrency MySQL แสดง authoritative transaction/constraints ส่วน Redis แสดง derived read/cache การตัดสินเงินต้องกลับไป owning context และ state ที่พิสูจน์ได้

อ่านเพิ่มเติม