ตอนที่ 5 ของซีรีส์ Domain-Driven Design ฉบับระบบธุรกิจจริง
Theme หลัก: ระบบบริหารพนักงาน (Staff / Employee Management System)
หลังจากตอนที่ 4 เราแยก Entity และ Value Object ได้แล้ว
ขั้นต่อไปที่สำคัญไม่แพ้กันคือคำถามนี้:
ใครมีสิทธิ์แก้ใครได้บ้าง?
ถ้าไม่กำหนดขอบเขตให้ชัด Domain จะเริ่ม “เละ” แบบไม่รู้ตัว
DDD ใช้แนวคิดนี้เพื่อแก้ปัญหา: Aggregate
ปัญหาที่เจอบ่อยในระบบพนักงาน
ลองดูสถานการณ์คุ้น ๆ:
- หน้าจอหนึ่งแก้
ShiftRequest.status - อีกหน้าหนึ่งแก้
approvedBy - Batch job มาแก้
approvedAt
ทุกอย่างเขียนถูก type
แต่สุดท้ายเกิดคำถามว่า:
- กฎยังครบไหม?
- ใครเป็นคนรับผิดชอบ?
- invariant ถูกทำลายไปหรือเปล่า?
นี่คือสัญญาณว่า ยังไม่มี Aggregate
Aggregate คืออะไร
Aggregate คือ:
กลุ่มของ Entity และ Value Object ที่ต้องถูกดูแลเป็นหน่วยเดียวกัน
โดยมี Aggregate Root เป็นประตูทางเข้าเพียงทางเดียว
หลักการสำคัญ:
- แก้ข้อมูลภายใน aggregate ได้ ผ่าน root เท่านั้น
- Root เป็นผู้ปกป้องกฎ (invariants)
Aggregate Root คืออะไร
Aggregate Root คือ Entity ตัวหลักที่:
- มี identity
- เป็นจุดเดียวที่ภายนอกเรียกใช้ได้
- รับผิดชอบความถูกต้องของทั้ง aggregate
ในระบบพนักงาน ตัวอย่าง Aggregate Root:
ShiftRequestStaff
ตัวอย่าง Aggregate: ShiftRequest
มาดูโครงสร้างแบบง่าย:
class ShiftRequest {
final ShiftRequestId id;
ShiftRequestStatus status;
StaffId staffId;
WorkingPeriod period;
Approval approval;
void approve(Staff approver) {
if (status != ShiftRequestStatus.pending) {
throw Exception('Only pending request can be approved');
}
approval = Approval.by(approver.id);
status = ShiftRequestStatus.approved;
}
}
Approval อาจเป็น Value Object ภายใน aggregate
ภายนอก ไม่ควร ไปแก้ approval ตรง ๆ
Invariant: กฎที่ห้ามถูกละเมิด
Invariant คือกฎที่ต้องเป็นจริง เสมอ ภายใน aggregate
ตัวอย่าง invariant ของ ShiftRequest:
- request ที่ approved แล้ว ห้ามกลับเป็น pending
- request หนึ่งรายการ อนุมัติได้เพียงครั้งเดียว
- approver ต้องมีสิทธิ์ตามกฎธุรกิจ
กฎเหล่านี้:
ต้องถูกบังคับโดย Aggregate Root
ไม่ใช่ controller หรือ service
ทำไม “ห้ามข้าม Aggregate”
ข้อผิดพลาดที่พบบ่อย:
request.approval.approvedBy = approverId; // ❌
โค้ดนี้:
- ข้าม root
- bypass กฎ
- ทำลาย invariant
ถ้าจำเป็นต้องเปลี่ยน:
request.approve(approver); // ✅
ขนาดของ Aggregate สำคัญมาก
Aggregate ที่ดีควร:
- เล็กที่สุดเท่าที่จำเป็น
- ไม่ดึงทุกอย่างมารวมกัน
ตัวอย่างที่ควรแยก:
Staffไม่ควรเป็น aggregate เดียวกับShiftRequest- ใช้
StaffIdอ้างอิงแทน object เต็ม
เหตุผล:
- performance
- concurrency
- ลด coupling
Repository ทำงานกับ Aggregate
Repository ควร:
- load / save ทั้ง aggregate
- ไม่ expose object ภายใน
abstract class ShiftRequestRepository {
ShiftRequest? findById(ShiftRequestId id);
void save(ShiftRequest request);
}
Repository ไม่ควรมี method อย่าง:
updateApprovalStatus()setApprovedBy()
นั่นคือการรั่วของ domain
สรุปตอนที่ 5
- Aggregate = ขอบเขตของความถูกต้อง
- Aggregate Root = ผู้เฝ้าประตู
- Invariant ต้องอยู่ใน aggregate
ถ้าคุณแก้ entity ไหนก็ได้จากที่ไหนก็ได้
นั่นแปลว่า domain ของคุณยังไม่มีเจ้าของ
ตอนต่อไป
ตอนที่ 6: Domain Service – เมื่อ logic ไม่ควรอยู่ใน Entity
เราจะดูกรณีที่ logic บางอย่างไม่เหมาะจะอยู่ใน entity แต่ก็ไม่ควรไปอยู่ใน service ทั่วไป

