บทที่ 2 · Part 1 — Architecture Starts with the Domain
Discovering the Domain
เปลี่ยน business journey ให้เป็น scenario, command, event, policy, invariant และ Ubiquitous Language ก่อนออกแบบ code
Discovering the Domain
ทีม Wallet ได้ requirement สั้น ๆ ว่า “เพิ่ม top-up ผ่านบัตร” หากเริ่มจาก endpoint
ทีมอาจรีบสร้าง POST /topups, ตาราง topups และ struct ชื่อ TopupService
แต่คำถามสำคัญยังไม่ถูกตอบ: ใครเริ่มรายการ เงินถือว่าเข้า Wallet เมื่อใด หาก provider
ตอบ timeout ต้องแสดงสถานะอะไร และคำว่า “สำเร็จ” ของ Checkout เหมือนกับของ Ledger หรือไม่
จบบทนี้คุณจะ
เปลี่ยน requirement สั้นให้เป็น business scenario ที่ตรวจสอบได้ แยก actor, command, event, policy และ invariant สร้าง glossary จากภาษาที่คนทำงานใช้ และระบุคำถามที่ยัง ห้ามแปลงเป็น code โดยใช้ Wallet top-up เป็นแบบฝึกหัด
[!IMPORTANT] วิธีอ่านหลักฐานในบทนี้ คำที่เป็น Definition/Pattern มีแหล่งต้นทางอยู่ใกล้ claim ส่วน decision table, starting structure, checklist และ lab เป็น Heuristic/Trade-off หรือ Course Convention สำหรับฝึกตัดสินใจ ไม่ใช่มาตรฐานสากล เว้นแต่บทจะระบุแหล่งและขอบเขตไว้ชัดเจน
เริ่มจาก Business Scenario
Domain discovery เริ่มจากสิ่งที่ธุรกิจพยายามทำ ไม่ใช่ชื่อ class ให้เขียน narrative ที่มีจุดเริ่ม จุดตัดสินใจ และผลลัพธ์ซึ่งคนจาก Product, Operations, Finance และ Engineering อ่านแล้วโต้แย้งได้ ตัวอย่างแรกอาจเป็น:
ลูกค้าที่ผ่านเงื่อนไขเริ่ม top-up จำนวน 50,000 สตางค์ผ่าน Checkout ระบบสร้าง Payment Attempt และขอให้ provider เรียกเก็บเงิน เมื่อได้รับหลักฐานที่เชื่อถือได้ว่า provider รับเงินแล้ว Wallet จึงบันทึก credit 1 ครั้ง หากผลลัพธ์ยังไม่ทราบ ระบบ คงรายการไว้เพื่อสอบถามและ reconciliation แทนการสรุปว่า failed
ข้อความนี้ยังไม่ใช่ specification ที่สมบูรณ์ แต่เผยคำที่ต้องตกลงทันที เช่น
Top-up, Payment Attempt, accepted, credit, unknown และ reconciliation
มันยังเปิดให้ถามว่าหลักฐานใด “เชื่อถือได้” และใครเป็นเจ้าของกฎนั้น
ใช้โครง 6 ช่องต่อ 1 scenario:
| ช่อง | คำถาม | ตัวอย่าง |
|---|---|---|
| Actor | ใครต้องการผลลัพธ์ | Customer |
| Trigger | อะไรเริ่มงาน | ยืนยัน top-up |
| Command | ผู้ใช้ขอให้ระบบทำอะไร | RequestTopUp |
| Decision | กฎใดอนุญาตหรือปฏิเสธ | wallet status, amount policy |
| Event | อะไรเกิดขึ้นแล้ว | TopUpRequested, FundsCredited |
| Exception | ความไม่แน่นอนคืออะไร | provider timeout, duplicate callback |
Command ใช้รูป imperative เพราะเป็นความตั้งใจที่อาจถูกปฏิเสธ ส่วน Event ใช้ past tense เพราะเป็นข้อเท็จจริงที่เกิดแล้ว นี่เป็น convention เพื่อให้สนทนาชัด ไม่ใช่กฎการตั้งชื่อ module ทั้งระบบ
Command, Event, Policy และ Invariant
อย่าเปลี่ยน sticky note ทุกใบเป็น class คำแต่ละชนิดตอบคำถามต่างกัน:
- Command มีผู้ขอ มีเป้าหมาย และมีเหตุผลที่จะ reject เช่น
ConfirmPayment - Event เป็นสิ่งที่ผู้เผยแพร่รับรองว่าเกิดแล้ว เช่น
PaymentConfirmed - Policy ตอบว่าเมื่อเกิดสิ่งหนึ่งแล้วควรทำอะไรต่อ เช่นเมื่อ payment confirmed จึงขอให้ Wallet credit
- Invariant คือเงื่อนไขที่ boundary เจ้าของกฎต้องไม่ยอมให้ผิด เช่น provider result เดียวต้องไม่เพิ่มเงินเข้า Wallet มากกว่า 1 ครั้ง
- Fact from another context คือข้อมูลที่เรารับมาภายใต้ contract ไม่ใช่ object ภายในที่มีความหมายเหมือนกันโดยอัตโนมัติ
ตัวเลข policy ไม่ควรถูกคิดขึ้นระหว่าง modelling
จำนวนขั้นต่ำ เพดาน top-up ระดับ KYC และเงื่อนไขการอนุมัติต้องมี accountable owner, เอกสาร และ effective date ในระบบจริง ตัวเลข 50,000 สตางค์ข้างต้นเป็นเพียง scenario input เพื่อฝึกเส้นทาง ไม่ใช่ข้อกำหนดทางธุรกิจหรือ compliance
Invariant ต้องเขียนให้ทดสอบได้ ประโยค “ห้ามเงินผิด” กว้างเกินไป แต่ประโยค “แต่ละ accepted top-up reference สร้าง Wallet credit ได้ไม่เกิน 1 รายการ” ทำให้ถามต่อได้ว่า reference มาจากใคร uniqueness อยู่ที่ใด และ retry จะเห็นผลเดิมอย่างไร
สร้าง Ubiquitous Language จากประโยคธุรกิจ
DDD Reference อธิบาย Ubiquitous Language ว่าเป็นภาษาที่ทีมใช้ในแบบจำลองและการสื่อสารประจำวัน ภาษานี้ไม่ใช่ data dictionary ซึ่งรวบรวมคำนามอย่างเดียว ให้เก็บทั้งคำกริยา state transition และประโยคตัวอย่าง:
| Term | Meaning in this context | Example statement | Avoid confusing with |
|---|---|---|---|
| Top-up | การเพิ่มมูลค่าเข้า Wallet จากแหล่งภายนอก | Customer requests a top-up | bank transfer เข้า settlement account |
| Payment Attempt | ความพยายามเรียกเก็บ 1 ครั้ง | Attempt becomes UNKNOWN after timeout | Checkout session |
| Credit | entry ที่เพิ่มผลต่อ balance | Wallet records one credit | provider capture |
| Confirmed | context ยอมรับหลักฐานแล้ว | Checkout confirms the attempt | Wallet credited |
หาก 2 ฝ่ายใช้คำเดียวต่างความหมาย อย่าบังคับให้เลือก definition กลางก่อนรู้ boundary
คำว่า Customer ใน Checkout อาจหมายถึงผู้ซื้อ ณ session หนึ่ง ขณะที่ Billing ต้องมี
billing profile, tax identity และ invoice address การมี 2 model ไม่ใช่ duplication
โดยอัตโนมัติ แต่เป็นหลักฐานว่าควรสำรวจ context
Glossary ที่ดีต้องมี owner และตัวอย่าง counterexample ด้วย Balance อาจเป็น posted balance,
available balance หรือ display balance หากทีมใช้คำเดียวแทนทั้ง 3 การตั้ง field ชัดขึ้น
ช่วยได้มากกว่าการสร้าง package ชื่อ domain
ไล่เส้นทาง Wallet Top-up
เริ่มจาก happy path แล้วจงใจเพิ่ม failure ที่เปลี่ยนความหมาย:
ถามทีละช่วง:
- Checkout สร้าง identifier ใดก่อนเรียก provider และ retry ใช้ identifier เดิมหรือไม่
- Provider timeout แปลว่าไม่ได้เงิน หรือแปลว่ายังไม่ทราบ
- Wallet เชื่อ command จาก Checkout หรือยืนยัน fact ผ่าน contract แบบใด
- หาก callback ซ้ำ Wallet ป้องกัน duplicate effect ที่ boundary ไหน
- Operations เห็นรายการค้างและแก้ไขด้วย business action อะไร
คำตอบยังไม่จำเป็นต้องเลือก REST, queue หรือ Temporal เพราะนั่นเป็น solution boundary ขั้นนี้ต้องระบุ semantic contract ก่อน หากทีมพูดว่า “ส่ง event แล้วจบ” ให้ถามว่า event ยืนยันข้อเท็จจริงอะไร ใครรับผิดชอบเมื่อ consumer ไม่ทำงาน และผู้ใช้เห็นสถานะใด
แบบฝึกปฏิบัติ: Discovery Sheet
เลือก top-up 1 scenario แล้วสร้างเอกสาร 1 หน้าโดยไม่วาด folder:
Goal:
Actor / trigger:
Preconditions:
Command:
Decisions and policy owners:
Events that become true:
Invariant that must never break:
Unknown or failure outcomes:
Terms with conflicting meanings:
Questions and named owners:
ใช้ three-pass review รอบแรกให้ Product ตรวจ goal และ language รอบที่ 2 ให้ Finance/Ops ตรวจเหตุการณ์และ exception รอบที่ 3 ให้ Engineering ตรวจว่า invariant ระบุ boundary และ observable evidence ได้หรือไม่ หาก review เริ่มถกชื่อ table ให้พักไว้ใน parking lot
คะแนนแบบฝึกหัด:
- ผ่าน เมื่อ scenario มี actor, command, at least one event, invariant และ unknown outcome
- ดี เมื่อศัพท์แต่ละคำมี example/counterexample และคำถามมี owner
- ควรทำใหม่ เมื่อเอกสารมีแต่ API/table หรือใช้คำว่า success โดยไม่มีความหมายเฉพาะ context
รายการตรวจสอบ
- เริ่มจาก journey ที่มี business outcome
- Command เป็นความตั้งใจ และ Event เป็นข้อเท็จจริงที่เกิดแล้ว
- Policy ระบุ owner แทนการสร้างเลขสมมติเป็น requirement
- Invariant เขียนเป็นเงื่อนไขที่ทดสอบหรือหาหลักฐานได้
- Glossary มี statement และ counterexample ไม่ใช่คำนามอย่างเดียว
- Unknown outcome ไม่ถูกบังคับให้กลายเป็น failed
- Technology decision ถูกพักไว้จน semantic contract ชัด
สรุปบทนี้
Domain discovery เปลี่ยน requirement ให้เป็นภาษาที่ผู้มีส่วนรับผิดชอบตรวจได้ Artifact แรกไม่ใช่ package diagram แต่เป็น scenario, glossary, policy ownership และ invariant ที่เฉพาะเจาะจง พอสิ่งเหล่านี้ชัด เราจึงมีหลักฐานสำหรับหา Subdomain และ Bounded Context ในบทถัดไป
อ่านเพิ่มเติม
- DDD Reference — Ubiquitous Language, Model-Driven Design และ Context
- Learning Domain-Driven Design, Chapter 2 — discovering domain knowledge และ communication
- EventStorming — แหล่งจากผู้สร้างวิธี; ใช้เป็น facilitation practice ไม่ใช่ข้อบังคับของ DDD