บทที่ 1 · Part 1 — Architecture Starts with the Domain
DDD Is Not a Folder Structure
แยก domain, model, code architecture, folder structure และ deployment boundary ก่อนเลือก pattern ให้ตรงปัญหา
ทีมหนึ่งกำลังเริ่ม Billing service ใหม่ ในห้องมีข้อเสนอ 3 แบบ คนแรกอยากสร้าง
domain, application, infrastructure, presentation ให้ครบก่อนเขียน use case
คนที่ 2 อยากแบ่ง controllers, services, repositories, models
ส่วนคนที่ 3 บอกว่า “ถ้าทำ DDD ต้องใช้ Clean Architecture”
ทั้ง 3 คนกำลังรีบตอบคำถามเรื่อง รูปทรงของ code ทั้งที่ยังไม่มีใครตอบว่า Billing ในบริษัทนี้หมายถึงอะไร ใครเป็นเจ้าของ Invoice, Credit Note และ Receipt กฎอะไรต้องเปลี่ยนพร้อมกัน หรือคำว่า Customer ใน Billing เหมือนกับ Customer ใน Wallet หรือไม่
จบบทนี้คุณจะ
- แยก Domain, Domain Model, Bounded Context, code architecture, folder structure และ deployment boundary ได้
- อธิบายได้ว่าเหตุใด DDD จึงไม่บังคับ Clean, Onion หรือ Hexagonal Architecture
- แยก sourced pattern, architecture heuristic และ course convention ออกจากกัน
- ตรวจ codebase จาก dependency และ ownership แทนการตัดสินด้วยชื่อ directory
- ใช้ Architecture Decision Card เลือกสิ่งที่ “พอดีตอนนี้” พร้อมสัญญาณให้ทบทวนภายหลัง
DDD แก้ปัญหาด้าน Modeling
คำว่า Domain หมายถึงพื้นที่ปัญหาหรือกิจกรรมของธุรกิจที่ software เข้าไปช่วย ส่วน Model คือการเลือกมุมมองและ abstraction ที่มีประโยชน์ต่อการแก้ปัญหานั้น Model จึงไม่ใช่การคัดลอกทุก column จาก database มาเป็น struct แต่เป็นการเลือก concept, relationship, rule และข้อยกเว้นที่ทีมจำเป็นต้องใช้คิดและสื่อสาร
Domain-Driven Design ให้เครื่องมือ 2 กลุ่มที่สัมพันธ์กัน:
- Strategic Design ช่วยเลือกสิ่งที่ธุรกิจควรลงทุน แบ่งขอบเขตที่ภาษาและ model 1 ชุดมีความหมาย และอธิบายความสัมพันธ์ระหว่างขอบเขต
- Tactical Design ช่วยสร้าง model ใน code ด้วยแนวคิดอย่าง Entity, Value Object, Aggregate, Domain Service, Repository และ Domain Event เมื่อแนวคิดเหล่านั้น เหมาะกับปัญหา
DDD Reference ของ Eric Evans รวบรวม pattern ทั้ง 2 กลุ่ม โดยมี Ubiquitous Language, Model-Driven Design, Bounded Context, Core Domain และ building blocks ของ model เป็นแกน สิ่งที่เอกสารนี้ ไม่ได้กำหนดคือชื่อ directory สากล ภาษาโปรแกรม framework หรือจำนวน service ที่ทุกทีมต้องใช้
หัวใจจึงไม่ใช่ว่า repository มีโฟลเดอร์ชื่อ domain หรือไม่ แต่คือคำถามเหล่านี้:
- ทีมกับ Domain Expert ใช้คำเดียวกันและให้ความหมายตรงกันหรือไม่
- Model ช่วยอธิบาย business rule ที่สำคัญจริงหรือไม่
- ขอบเขตของภาษาและ rule ชัดพอที่จะป้องกันความหมายปะปนหรือไม่
- ส่วนที่สร้างความแตกต่างให้ธุรกิจได้รับความสนใจมากกว่าส่วนทั่วไปหรือไม่
- Code ทำให้สถานะที่ผิดกฎเกิดขึ้นง่ายหรือยาก
Definition ไม่เท่ากับ implementation convention
Aggregate และ Bounded Context เป็นคำใน DDD แต่ internal/domain,
usecases/ หรือ adapters/mysql/ เป็น convention ที่ทีมเลือกเพื่อสื่อ architecture
2 project อาจใช้ DDD principles เหมือนกันแต่จัด package ต่างกันได้
6 การตัดสินใจที่มักถูกปนกัน
การถกเรื่อง architecture มักยาวเพราะผู้ร่วมวงกำลังตอบคนละคำถาม แต่ใช้คำว่า “แบ่งระบบ” เหมือนกันทั้งหมด ลองแยกเป็น 6 ระดับ:
| ระดับการตัดสินใจ | คำถาม | ตัวอย่างคำตอบ |
|---|---|---|
| Business strategy | ความสามารถใดสร้างความแตกต่างและควรลงทุน | Pricing model เป็น Core Subdomain |
| Model boundary | ภาษาและ model ชุดนี้ใช้ได้ถึงตรงไหน | Customer ใน Billing คือผู้รับเอกสาร ไม่ใช่ Wallet Holder |
| Business-logic pattern | จะจัดรูป rule ใน code อย่างไร | Transaction Script หรือ Domain Model |
| Application architecture | inside คุยกับ outside ผ่าน boundary แบบใด | Layered หรือ Ports & Adapters |
| Code organisation | คนหา code ที่เปลี่ยนพร้อมกันเจอที่ไหน | package by feature แล้วแบ่ง role ภายใน slice |
| Deployment topology | process ไหน deploy, scale และ fail แยกกัน | 1 modular service หรือหลาย services |
ทุกระดับส่งผลต่อระดับถัดไป แต่ไม่มีความสัมพันธ์แบบหนึ่งต่อหนึ่ง ตัวอย่างเช่น Bounded Context หนึ่งอาจเริ่มด้วยหลาย Go packages ใน process เดียว และใช้ MySQL schema เดียว ที่มีกติกา ownership ชัด เมื่อ load, team หรือ failure isolation เปลี่ยนจึงค่อยแยก deployable
ในทิศกลับกัน microservice 2 ตัวอาจใช้ model และ database table ชุดเดียวกันจนไม่มี model boundary จริง แม้ deployment diagram จะดูแยกกันแล้วก็ตาม
ลูกศรในภาพคือ ลำดับคำถาม ไม่ใช่คำสั่งว่าทุกขั้นต้องสร้าง artifact แยกกัน module เล็กอาจตอบทั้งหมดในหน้าเดียว แต่การตอบจากบนลงล่างช่วยป้องกันไม่ให้ technology เป็นผู้กำหนด model โดยไม่ตั้งใจ
Architecture Pattern เป็นเครื่องมือ
หนังสือ Learning Domain-Driven Design บทที่ 5 เริ่ม implementation patterns ด้วย Transaction Script และ Active Record สำหรับ business logic ที่ค่อนข้างเรียบง่าย ก่อนเข้าสู่ pattern สำหรับ logic ที่ซับซ้อนกว่าในบทถัดไป นี่เป็นหลักฐานสำคัญว่า “ใช้ DDD” ไม่ได้แปลว่า “ทุก module ต้องมี Aggregate และ Repository”
Martin Fowler อธิบาย trade-off ระหว่าง Transaction Script กับ Domain Model ว่า Domain Model มีต้นทุนเริ่มต้นด้านโครงสร้างและ data access ซึ่งคุ้มเมื่อมี domain logic มากพอ ไม่ใช่เพราะ Domain Model มีสถานะสูงกว่าเสมอ ดูการเปรียบเทียบจาก Domain Logic and SQL
ลองเทียบ 2 capability:
การตั้งค่าการแจ้งเตือน
กฎมีเพียงผู้ใช้เลือกเปิดหรือปิด email และ SMS โดยแต่ละ request validate input แล้วบันทึกค่า หากไม่มี lifecycle ซับซ้อน ไม่มีการเปลี่ยนหลาย Aggregate และไม่มี policy แตกแขนง Transaction Script ที่ตั้งชื่อตาม use case อาจตรงกว่า model ที่สร้าง Entity, Factory, Repository interface และ Domain Event ครบชุด
การใช้ DDD ในที่นี้ยังทำได้:
- ตกลงว่า
DisableMarketingEmailต่างจากDisableSecurityAlertอย่างไร - ใช้ภาษาธุรกิจใน command และ field
- ระบุ policy ว่า security alert ปิดได้หรือไม่ได้
- วาง transaction boundary ไม่ให้บันทึกสถานะครึ่งเดียว
การอนุมัติ Refund
Refund อาจขึ้นกับ payment state, refundable amount, refund ก่อนหน้า, currency, maker-checker policy และ provider outcome กฎเหล่านี้เปลี่ยนร่วมกันและต้องป้องกัน สถานะที่ผิดแม้มี request พร้อมกันหลายตัว การสร้าง model ที่ซ่อน invariant และแยก external provider ออกจาก business language จึงเริ่มมีผลตอบแทน
Domain Model ไม่ได้ป้องกัน concurrent refund ได้ด้วยตัวเอง
Model มีหน้าที่แสดง invariant เช่นยอดรวมที่ refund ต้องไม่เกิน refundable amount แต่ถ้า request 2 ตัวอ่าน state เดียวกันพร้อมกัน ทั้งคู่อาจผ่านกฎก่อนเขียนข้อมูล ระบบจึงยังต้องมี concurrency boundary ที่สอดคล้องกัน เช่น MySQL transaction พร้อม locking หรือ optimistic concurrency, database constraint ที่ทำได้จริง และ idempotency ที่ application boundary วิธีที่เลือกต้องพิสูจน์ด้วย concurrent test ตาม failure mode ของ datastore ไม่ใช่อาศัยชื่อ Aggregate เพียงอย่างเดียว
ความต่างไม่ได้อยู่ที่ชื่อ subdomain เพียงอย่างเดียว แต่อยู่ที่ ความหนาแน่นของ rule, ความผูกพันของ state, ความถี่การเปลี่ยน และผลกระทบเมื่อผิด
Core ไม่ได้แปลว่า critical ที่สุดเสมอ
Core Subdomain หมายถึงสิ่งที่สร้างความได้เปรียบหรือความแตกต่างของธุรกิจ ระบบ authentication หรือ notification อาจไม่ใช่ Core แต่ยังมีผลกระทบรุนแรงเมื่อพัง จึงต้องลงทุนด้าน security และ reliability ตาม risk โดยไม่ต้องแกล้งเรียกมันว่า Core
Hexagonal ไม่ใช่ตรารับรอง DDD
Hexagonal Architecture ต้นฉบับของ Alistair Cockburn ต้องการให้ application ทำงานและทดสอบได้โดยไม่ผูกกับ UI หรือ database ที่ใช้จริง แนวคิดหลักคือ application ด้านในสื่อสารกับสิ่งด้านนอกผ่าน ports และมี adapters แปลง technology-specific interaction เข้าหา application
Ports & Adapters จึงตอบคำถามเรื่อง application boundary และ dependency ขณะที่ DDD ตอบคำถามเรื่อง model, language, strategic investment และ context boundary สองแนวคิดสนับสนุนกันได้ดี แต่ไม่ใช่คำเดียวกัน:
- ใช้ Ports & Adapters โดยไม่มี Domain Model ก็ได้ เช่น application ที่มี Transaction Script แต่ต้องสลับ database หรือ external API หลายราย
- ใช้ Domain Model โดยยังไม่สร้าง port ทุก dependency ก็ได้ หาก module เล็กและ dependency ไม่มี volatility หรือ test seam ที่คุ้มต้นทุน
- ใช้ทั้งคู่เมื่อมี business rules ซับซ้อนและ outside dependency ที่ไม่ควรกำหนดภาษาใน core
Onion Architecture และ Clean Architecture มีแนวคิด dependency ชี้เข้าด้านในคล้ายกัน แต่มีที่มา คำเรียก และรายละเอียดต่างกัน บทที่ 10 จะเปรียบเทียบโดยตรง ในตอนนี้ให้จำว่า ชื่อ architecture ไม่ใช่ใบรับรองว่า code ทำ DDD ถูกต้อง
โครงสร้าง Folder เดียวกันอาจหลอกตา
สมมติ repository มีโครงสร้างที่ดูเหมือน Clean Architecture:
internal/payment/
domain/
application/
ports/
adapters/
ถ้า domain import Stripe SDK, application ส่ง Stripe status ออกเป็น public response
และ adapter เขียน field ของ Entity ข้าม invariant โดยตรง โฟลเดอร์เหล่านี้ไม่ได้ปกป้อง
business model เลย มันเป็นเพียงป้ายชื่อ
// package domain แต่ผูก business model กับ vendor และ storage โดยตรง
type Payment struct {
StripePaymentIntent *stripe.PaymentIntent
Row *PaymentRow
}
ในทางกลับกัน codebase ที่มี package เดียวอาจรักษา boundary ได้ดีกว่า หาก business types ไม่รู้จัก transport/vendor, use case เป็นเจ้าของ interface ที่ต้องใช้ และ composition root เป็นจุดประกอบ implementation
package payment
type Gateway interface {
CreatePayment(ctx context.Context, cmd CreatePayment) (PaymentOutcome, error)
}
type Service struct {
gateway Gateway
}
Go ไม่ต้องมีคำว่า implements ตัว adapter เพียงมี method set ตรงกับ interface
ประเด็นสำคัญกว่าตำแหน่งไฟล์คือ:
- ใครเป็นเจ้าของ vocabulary ของ interface
- dependency ชี้จาก implementation ไปหา contract หรือกลับกัน
- vendor type หยุดอยู่ตรง boundary จริงหรือไม่
- business invariant ถูกตรวจจากเส้นทางเข้าได้ครบทุกทางหรือไม่
- code ที่เปลี่ยนด้วยเหตุผลเดียวกันอยู่ใกล้กันหรือไม่
ตรวจ import ก่อนตรวจ tree
เมื่อมีคนบอกว่า codebase เป็น Clean หรือ Hexagonal ให้ดู import graph และเลือก use case 1 เส้นเดินตั้งแต่ HTTP ถึง database/provider ชื่อ directory บอก “เจตนา” แต่ dependency และ runtime behavior บอก “สิ่งที่เกิดขึ้นจริง”
ชื่อ Product Fintech ยังไม่ใช่ Boundary
บริษัทหนึ่งอาจมี Product ชื่อ Wallet, Checkout และ Billing พร้อม Web Portal กลาง แต่ชื่อใน organisation chart ยังไม่ใช่ Bounded Context โดยอัตโนมัติ
คำว่า Customer เป็นตัวอย่างที่ดี:
| พื้นที่ | Customer อาจหมายถึง | Rule ที่สนใจ |
|---|---|---|
| Wallet | ผู้ถือ wallet ที่ผ่านระดับ identity 1 | เปิด/ระงับ account และควบคุมเงิน |
| Checkout | ผู้กำลังจ่ายใน checkout session | payment method และ customer action |
| Billing | ผู้รับ invoice หรือผู้มีหน้าที่ชำระ | billing profile, tax identity และ payment terms |
| Portal | actor ที่กำลังดูหรือจัดการข้อมูล | permission, tenant และ presentation preference |
หากบังคับให้ทุกพื้นที่ใช้ Customer struct เดียว ความหมายจะโตจน field จำนวนมากมีค่า
เฉพาะบาง flow และการเปลี่ยน Billing อาจกระทบ Wallet โดยไม่จำเป็น Strategic DDD ช่วยถามว่า
ภาษาแต่ละชุดใช้ได้ถึงตรงไหน และ integration ต้องแปลความหมายอย่างไร
จากนั้น code architecture จึงเข้ามาช่วยรักษาคำตอบ เช่น package ownership, public contract, Anti-Corruption Layer และ dependency test แต่ architecture ไม่สามารถ ค้นพบ business meaning แทนการคุยกับ Domain Expert ได้
ลำดับการตัดสินใจที่ดีกว่า
เมื่อต้องออกแบบ module ใหม่ ให้เลื่อนคำถามเรื่อง folder ไปท้ายลำดับ:
- Scenario — actor ต้องการผลลัพธ์อะไร และเหตุการณ์ใดเริ่ม use case
- Language — คำสำคัญหมายถึงอะไร มีคำใดกำกวมระหว่างทีม
- Policy — กฎใดต้องจริงเสมอ ใครเป็นเจ้าของ และหลักฐานอยู่ที่ไหน
- Boundary — state และ rule ใดต้องเปลี่ยนอย่างสอดคล้อง ใครเป็นเจ้าของข้อมูล
- Forces — logic ซับซ้อนและเปลี่ยนบ่อยแค่ไหน มี external dependency หรือไม่
- Pattern — Transaction Script, Domain Model, Layered หรือ Ports & Adapters ช่วยลดความเสี่ยงใด
- Code shape — package, file, interface และ composition root สื่อ boundary อย่างไร
- Deployment — มีเหตุผลด้าน scale, team, security หรือ failure isolation ที่ต้องแยก process แล้วหรือยัง
ลำดับนี้ไม่ได้ห้าม refactor หากข้อมูลใหม่ทำให้ model เปลี่ยน DDD มอง modeling เป็นกิจกรรม ต่อเนื่อง และ architecture ที่ดีควรมี exit criteria ว่าเมื่อใดแบบปัจจุบันจะไม่พอ
ตัวอย่างเช่น:
“เริ่ม Notification Preference ด้วย Transaction Script ใน feature package เดียว เพราะมี 2 use cases และ policy ยังไม่แตกแขนง หาก channel มี lifecycle ต่างกัน, policy ซ้ำเกิน 2 จุด หรือมีการเปลี่ยน state ข้าม entity แบบ atomic เราจะทบทวน Domain Model”
ประโยคนี้ป้องกันทั้ง overengineering วันนี้และการยึดติดกับ design เดิมในอนาคต
บัตรบันทึก Architecture Decision
ใช้ card นี้ก่อนสร้าง directory ใหม่:
Capability:
Business outcome:
Ubiquitous Language:
Rules that must never be violated:
State that must change consistently:
External dependencies and failure modes:
Expected change and team ownership:
Security / compliance / operational risk:
Simplest adequate business-logic pattern:
Required ports or seams:
Chosen code organisation:
Chosen deployment boundary:
What this design deliberately does not solve:
Evidence that would trigger a redesign:
Policy owners and approval dates:
ช่อง What this design deliberately does not solve สำคัญ เพราะทำให้คำว่า “เรียบง่าย”
ไม่กลายเป็นข้ออ้างซ่อนความเสี่ยง ส่วน Evidence that would trigger a redesign
ทำให้ architecture เป็นสมมติฐานที่ตรวจสอบได้ ไม่ใช่คำสาบานถาวร
แบบฝึกปฏิบัติ — จำแนกก่อนวาด Folder
สมมติทีมได้รับ requirement ว่า:
“Merchant ตั้ง subscription plan ได้ ระบบรับ usage รายวัน ออก invoice สิ้นรอบ คำนวณ tax ผ่าน provider ภายนอก รับชำระผ่าน payment provider ออก receipt และให้ finance operator ดูทุกอย่างจาก Web Portal”
ใช้เวลา 25 นาทีและทำตามลำดับ:
- วงคำที่ยังไม่รู้ความหมาย เช่น
usage,ออก invoice,รับชำระ,receipt - เขียนอย่างน้อย 3 policy ที่ต้องถาม Domain Expert ห้ามคิดค่าหรือเงื่อนไขขึ้นเอง
- แยกการตัดสินใจแต่ละข้อเป็น business strategy, model boundary, business-logic pattern, application architecture, code organisation หรือ deployment
- เลือก capability หนึ่งที่น่าจะเริ่มด้วย Transaction Script และ 1 capability ที่น่าจะต้อง Domain Model พร้อมระบุหลักฐาน
- เลือก outside dependency 1 ตัวที่ควรมี port พร้อมเขียน vocabulary ของฝั่ง application
- วาด folder structure เป็นขั้นสุดท้าย แล้วตรวจว่าแต่ละ directory มีเหตุผลในการเปลี่ยน ที่ชัดเจนหรือไม่
เกณฑ์ประเมินตนเอง
| เกณฑ์ | ผ่านเมื่อ |
|---|---|
| Language | คำสำคัญมีความหมายในประโยคธุรกิจ ไม่ใช่แค่ชื่อ table |
| Boundary | อธิบาย ownership และ invariant ได้ ไม่ได้แบ่งตาม UI screen อย่างเดียว |
| Pattern | มี force รองรับ ไม่ได้เลือกเพราะเป็น “best practice” |
| Dependency | application vocabulary ไม่ขึ้นกับชื่อ class/status ของ provider |
| Data | ระบุ authoritative owner และสิ่งที่เป็นเพียง read/cache ได้ |
| Security | แยก identity, permission, ownership และ business policy ได้ |
| Evolution | มีสัญญาณที่วัดหรือสังเกตได้ว่าจะทบทวน design เมื่อไร |
ถ้าตอบข้อใดด้วยชื่อ technology อย่างเดียว เช่น “ใช้ Redis”, “ใช้ microservice” หรือ “ใช้ Clean Architecture” ให้ย้อนกลับไปเติมปัญหาและ force ที่ technology นั้นกำลังแก้
รูปแบบความล้มเหลวที่พบบ่อย
ออกแบบโดยเริ่มจาก Folder
สร้าง directory จากภาพ template ก่อนรู้ use case ผลคือทุก feature ต้องกระจายแก้หลายที่ และชื่อ layer บัง language ของธุรกิจ
สะสม Pattern
ใส่ Entity, Value Object, Repository, Factory และ Domain Event เพราะกลัว “ทำ DDD ไม่ครบ” แต่ไม่มี invariant หรือ change pressure ที่ทำให้แต่ละ abstraction มีงานจริง
Core แปลว่าต้องซับซ้อน
ตั้งใจทำ Core ให้ซับซ้อนเพื่อแสดงการลงทุน ทั้งที่เป้าหมายคือทำให้ business complexity มองเห็นและแก้ได้ ไม่ใช่เพิ่ม technical ceremony
Context เท่ากับ Service
สร้าง network boundary, deployment pipeline และ distributed transaction ก่อนภาษาและ ownership นิ่ง แล้วต้องแก้ contract ข้ามทีมทุกครั้งที่เรียนรู้ domain เพิ่ม
Framework เป็นเจ้าของภาษา
ตั้งชื่อ use case และ model ตาม controller, ORM หรือ provider SDK จน Domain Expert อ่านแล้วไม่รู้ว่าระบบกำลังทำอะไร
รายการตรวจสอบ
- เริ่มจาก business scenario ก่อน package diagram
- แยก Strategic DDD, Tactical DDD และ application architecture ได้
- แยก Bounded Context ออกจาก service/repository/database ได้
- เลือก pattern จาก complexity, change, consistency, dependency และ risk
- อธิบายได้ว่าเหตุใด module เรียบง่ายจึงยังใช้ DDD thinking ได้
- ตรวจ import direction และ runtime path ไม่เชื่อชื่อ directory อย่างเดียว
- ระบุว่าอะไรคือ sourced pattern, heuristic และ course convention
- มี owner สำหรับ business/compliance policy ที่กระทบเงินหรือสิทธิ์
- มี exit criteria สำหรับ architecture ที่เลือกวันนี้
สรุปบทนี้
DDD ช่วยให้ทีมสร้าง language, model และ boundary ที่ตรงกับปัญหาธุรกิจ ส่วน Transaction Script, Domain Model, Layered Architecture และ Ports & Adapters เป็นเครื่องมือจัด code ที่เลือกตาม force ของแต่ละ module Folder structure เป็นเพียง วิธีสื่อการเลือกนั้น และ deployment เป็นอีกการตัดสินใจหนึ่ง เริ่มจาก scenario กับ rule, เลือก pattern ที่พอดี แล้วจึงวาด directory
อ่านเพิ่มเติม
- DDD Reference — pattern summaries โดย Eric Evans
- Domain-Driven Design — Eric Evans
- Learning Domain-Driven Design, Chapter 5 — simple business-logic patterns และการเลือกให้เหมาะกับ subdomain
- Implementing Domain-Driven Design — เชื่อม Strategic/Tactical DDD กับ architecture และ application
- Domain Logic and SQL — trade-off ระหว่าง Transaction Script, Domain Model และ SQL
- Hexagonal Architecture — บทความต้นฉบับของ Ports & Adapters
- Go Code Review Comments: Interfaces — official Go guidance เรื่อง consumer-owned interface และ implicit implementation