Decompose by Subdomain
คุณแบ่ง service รอบแรกไปแล้ว อาจจะวาดเส้นด้วย business capability และเส้นแบ่งก็ดูเข้าท่าอยู่ แต่ โมเดล กลับรั่วข้ามเส้นเหล่านั้น คำว่า “customer” หมายถึง billing account ใน service หนึ่ง หมายถึงผู้รับสินค้าในอีก service หนึ่ง และหมายถึง marketing lead ในตัวที่สาม แต่ทั้งสามกลับใช้ object Customer ตัวเดียวที่บวมเป่งร่วมกัน ทีมไหนจะแตะ object นี้ก็ต้องเข้าใจกฎของทุกทีมที่เหลือ คุณจึงต้องการวิธีที่มีวินัยกว่านี้ ไม่ใช่แค่ตัดสินว่าเส้นแบ่ง service อยู่ตรงไหน แต่ตัดสินว่าโมเดลของแต่ละ service คือ อะไรกันแน่
domain model เดียวที่ครอบทั้งระบบฟังดูน่าหลงใหล และเกือบทุกครั้งกลายเป็นกับดัก ยิ่งเพิ่มแนวคิดและกฎเข้าไปใน entity ที่ใช้ร่วมกันมากเท่าไร โมเดลก็ยิ่งคลุมเครือ คำเดียวกันมีความหมายไม่เหมือนกันในแต่ละส่วนของธุรกิจ พอพยายามหาข้อประนีประนอม ผลที่ได้คือ class ที่ไม่ถูกใจใครสักฝ่ายและเปลี่ยนไม่หยุด นี่คือปัญหา large monolithic domain model ความคลุมเครือนี้ไม่ใช่ความบังเอิญที่รอเก็บกวาดทีหลัง แต่สะท้อนความจริงว่าแต่ละส่วนของธุรกิจมองคำเดียวกันคนละแบบ และต่างก็มองอย่างมีเหตุผลของตัวเอง
แรงที่ขัดกันจึงเป็นแบบนี้ คุณอยากให้แต่ละ service มีโมเดลที่ชัด สอดคล้องกันภายใน และเล็กพอจะทำความเข้าใจได้ทั้งก้อน แต่ก็ต้องยอมรับว่าไม่มีโมเดลเดียวไหนรับใช้ทั้งธุรกิจได้โดยไม่จืดจนไร้ความหมาย
วิธีแก้
หัวข้อที่มีชื่อว่า “วิธีแก้”Domain-Driven Design (DDD) มีคำศัพท์ไว้อธิบายปัญหานี้อยู่แล้ว พื้นที่ปัญหาทั้งหมด — ทุกอย่างที่ธุรกิจเข้าไปเกี่ยวข้อง — เรียกว่า domain และแบ่งย่อยลงเป็น subdomain ซึ่งคือพื้นที่ความเชี่ยวชาญที่แยกจากกัน เช่น การรับ order, inventory, billing, การจัดส่ง subdomain แบ่งได้หลายประเภทและช่วยชี้ว่าควรลงทุนตรงไหน subdomain แบบ core คือสิ่งที่สร้างความแตกต่างให้ธุรกิจและควรได้วิศวกรที่เก่งที่สุด subdomain แบบ supporting จำเป็นแต่ไม่ใช่จุดขาย ส่วน subdomain แบบ generic อย่าง authentication หรือ payments มักซื้อมาใช้คุ้มกว่าสร้างเอง
แนวคิด DDD ที่สำคัญที่สุดต่อการ decompose คือ bounded context — เส้นแบ่งที่ชัดเจนซึ่งภายในนั้น domain model หนึ่งชุดมีผลบังคับใช้ และทุกคำมีความหมายแม่นยำเพียงความหมายเดียว ภายใน billing context คำว่า “customer” หมายถึง billing account เท่านั้น ไม่ต้องแก้ตัวและไม่ต้องหาคำนิยามกลางที่ใช้ร่วมกัน พ้นเส้นแบ่งออกไป คำนี้อาจหมายถึงอย่างอื่น ซึ่งไม่มีปัญหา เพราะเส้นแบ่งทำให้ความต่างนั้นเห็นชัด
pattern นี้คือ จับ bounded context หนึ่งให้ตรงกับ subdomain หนึ่ง แล้ว implement แต่ละ bounded context เป็น service ตรงจุดที่สอง context ต้องทำงานร่วมกัน ให้ออกแบบการแปลงระหว่างโมเดลอย่างจงใจ ผ่าน published contract หรือ anti-corruption layer แทนที่จะปล่อยให้โมเดลฝั่งหนึ่งซึมเข้าไปในอีกฝั่ง
flowchart TB
subgraph Domain[Domain: e-commerce]
direction LR
subgraph BCOrder[Bounded Context: Ordering<br/>core]
OCust[Customer = buyer placing an order]
OOrder[Order, OrderLine]
end
subgraph BCBill[Bounded Context: Billing<br/>supporting]
BCust[Customer = billing account]
BInv[Invoice, Payment]
end
subgraph BCShip[Bounded Context: Delivery<br/>supporting]
SCust[Customer = recipient address]
SShip[Shipment, Tracking]
end
end
BCOrder -. published contract .-> BCBill
BCOrder -. published contract .-> BCShip เทียบกับ Decompose by Business Capability
หัวข้อที่มีชื่อว่า “เทียบกับ Decompose by Business Capability”สองกลยุทธ์นี้เสริมกันมากกว่าจะแข่งกัน และมักได้คำตอบตรงกัน ผลลัพธ์ที่ได้มักเป็นชุด service ที่คล้ายกัน เพราะ business capability กับ subdomain อธิบายส่วนเดียวกันของธุรกิจจากคนละมุม capability ถามว่า “ธุรกิจ ทำ อะไร” ส่วน subdomain ถามว่า “งานนั้นต้องใช้ ความรู้ด้านไหน และโมเดลชุดใดมีผลตรงนั้น” ความต่างในทางปฏิบัติอยู่ที่ความเข้มงวดและจุดที่เน้น
- แบ่งด้วย business capability เริ่มได้เร็วกว่าและเข้าใจง่ายกว่า ให้การแบ่งรอบแรกที่สมเหตุสมผลจากมุมองค์กร แต่หยุดแค่นั้น ไม่ได้ลงไปนิยามโมเดลภายในของแต่ละ service
- แบ่งด้วย subdomain เข้มงวดกว่าและขับเคลื่อนด้วยโมเดล ทำงานเพิ่มอีกขั้นด้วยการตรึงโมเดลที่ไม่กำกวมหนึ่งชุดต่อหนึ่ง context นี่คือสิ่งที่กันปัญหา shared-model leakage ที่การแบ่งด้วย capability อย่างเดียวแก้ไม่ได้
แนวทางที่ใช้กันบ่อยและได้ผลคือใช้ capability ร่างรอบแรก แล้วใช้ DDD เกลาให้คม โดยเฉพาะรอบ ๆ core subdomain ที่เป็นจุดที่การวางโมเดลให้ถูกสำคัญที่สุด
ผลลัพธ์ที่ตามมา
หัวข้อที่มีชื่อว่า “ผลลัพธ์ที่ตามมา”สิ่งที่คุณได้:
- หนึ่ง service หนึ่งโมเดลที่ชัดเจน ภายใน bounded context ทุกคำมีความหมายเดียว code จึงเล็กลง กฎอยู่ถูกที่ และทีมเลิกเถียงกันเรื่องคำนิยามกลางที่ไม่มีวันถูกใจทุกฝ่าย
- เส้นแบ่งชัดและมีเหตุผลรองรับ DDD เปลี่ยนการวาดเส้นให้เป็นการตัดสินใจเชิงโมเดลที่คุณอธิบายได้ และบังคับให้ความสัมพันธ์ระหว่าง service เกิดจากการออกแบบ ไม่ใช่บังเอิญโผล่ขึ้นมา
- รู้ว่าควรลงทุนตรงไหน การจัด subdomain เป็น core, supporting หรือ generic บอกได้ว่าตรงไหนควรสร้างเอง ตรงไหนควรซื้อ และควรวางคนเก่งที่สุดไว้ตรงไหน
สิ่งที่คุณต้องจ่าย:
- เรียนรู้ยากจริง DDD มีศัพท์เยอะและต้องทำงานใกล้ชิดกับ domain expert ทีมที่เพิ่งเริ่มมักใช้แนวคิดผิดในช่วงแรก
- มีงานแปลงโมเดลเพิ่ม เพราะแต่ละ context หวงโมเดลของตัวเอง การทำงานข้าม context จึงต้อง map อย่างชัดเจน ผ่าน published contract หรือ anti-corruption layer ซึ่งลงแรงล่วงหน้ามากกว่าการใช้โมเดลเดียวร่วมกัน
- เส้นแบ่งยังขยับได้อยู่ดี subdomain ชัดกว่า capability แต่ก็ไม่ได้อยู่นิ่ง พอเข้าใจ domain ลึกขึ้นก็ต้องปรับ context ตามไปด้วย เตรียมใจไว้ได้เลย
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”- Decompose by Business Capability — กลยุทธ์เสริมที่ให้การแบ่งรอบแรกอย่างรวดเร็ว แล้วค่อยใช้ DDD เกลาให้คม
- Self-Contained Service — bounded context ให้โมเดลในที่ (local model) ที่แต่ละ service ต้องใช้เพื่อตอบ request ได้ด้วยตัวเอง
- ภาพรวมการ decompose — กลยุทธ์นี้อยู่ตรงไหนในภาพใหญ่
| ข้อดี | ข้อแลกเปลี่ยน |
|---|---|
| service boundary มาจาก domain model — สะท้อน business reality | ต้องการ Domain-Driven Design expertise |
| Ubiquitous Language ชัดเจนในแต่ละ service | bounded context analysis ใช้เวลา |
| ลด impedance mismatch ระหว่าง business และ code | subdomain อาจ overlap — ต้องตัดสินใจว่า capability ใดอยู่ subdomain ไหน |
| เหมาะกับ domain ซับซ้อนที่มี business rule เยอะ | ไม่เหมาะถ้าทีมไม่มี DDD background |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”Subdomain = Table — map database table หนึ่งตัวเป็น service หนึ่งตัว อาการ:
- มี UserService, AddressService, PhoneService แยกกัน
- operation ง่าย ๆ ต้องเรียก 3 service
- ไม่ใช่ subdomain decomposition แต่เป็น database decomposition
Ignoring Anti-corruption Layer — รับ model จาก subdomain อื่นมาตรงโดยไม่ transform อาการ:
- change ใน Order subdomain ส่งผลถึง Inventory subdomain โดยตรง
- boundary แยกอยู่แต่ concept รั่วข้าม
💡 ตัวอย่างจากของจริง
Zalando:
- ใช้ DDD + subdomain decomposition สำหรับ e-commerce platform
- subdomain: catalog, checkout, fulfillment, returns, customer
- แต่ละ subdomain มี Ubiquitous Language ของตัวเอง — “article” ใน catalog ≠ “item” ใน fulfillment
Shopify:
- core domain: commerce (checkout, payment)
- supporting domain: inventory, shipping
- generic domain: authentication, notification