Aggregate Pattern
กลุ่ม entity ที่มี root เดียวคุมการเข้าถึงและความสอดคล้อง
Aggregate คือ กลุ่มของ entity (และอาจมี value object) ที่เกี่ยวข้องกันในเชิงธุรกิจ ซึ่งถูกปฏิบัติเป็นหน่วยเดียวสำหรับการเปลี่ยนแปลงข้อมูลและการทำธุรกรรม แต่ละ aggregate มี entity รากเดียวเรียกว่า aggregate root ซึ่งเป็นจุดเข้าเพียงจุดเดียวจากภายนอก code ภายนอกจะอ้างอิงหรือเรียก method ได้เฉพาะผ่าน root เท่านั้น ไม่ใช่เข้าถึง entity ลูกโดยตรง
Martin Fowler อธิบายไว้อย่างกระชับว่า aggregate คือ “a cluster of domain objects that can be treated as a single unit” — ตัวอย่างคลาสสิกคือ order พร้อม order line items ที่แม้เป็นคนละ object แต่ถูกจัดการร่วมกันเป็นหนึ่งเดียว Fowler ยังเน้นว่า reference จากภายนอกควรชี้ไปที่ root เท่านั้น และการโหลด/บันทึกข้อมูลควรทำเป็นหน่วย aggregate ทั้งก้อน โดย ธุรกรรมไม่ควรข้ามขอบเขต aggregate
ข้อควรระวังที่ Fowler เน้นย้ำคือ อย่าสับสน DDD aggregate กับ collection class ทั่วไป (list, map ฯลฯ) — aggregate เป็นแนวคิดทาง domain ที่มีความหมายทางธุรกิจ (order, การนัดตรวจคนไข้, playlist) ไม่ใช่โครงสร้างข้อมูลทั่วไป
สิ่งสำคัญคือ root ต้องเป็น entity เสมอ (ไม่ใช่ value object) เพราะต้องมี identifier ไว้บันทึกและดึงข้อมูลจาก data store ได้ การรวมกลุ่มเป็น aggregate ยังช่วยเลี่ยงปัญหาการ eager-load ข้อมูลทั้งฐานข้อมูลโดยไม่จำเป็น
บทบาทใน DDD
หัวข้อที่มีชื่อว่า “บทบาทใน DDD”Aggregate เป็นหนึ่งใน tactical pattern หลักของ DDD ร่วมกับ entity, value object, domain service และ domain event บทบาทของมันคือการกำหนด consistency boundary — ขอบเขตที่ invariant ทางธุรกิจต้องเป็นจริงเสมอหลังจบธุรกรรมหนึ่ง ๆ
Vaughn Vernon สรุปไว้ใน Effective Aggregate Design (IDDD) เป็นสี่กฎสำคัญ:
- ปกป้อง invariant ที่แท้จริงภายในขอบเขตความสอดคล้อง — หนึ่งธุรกรรมควรแก้ไขได้เพียง1 aggregate เท่านั้น
- ออกแบบ aggregate ให้เล็ก — ยิ่ง aggregate ใหญ่ ยิ่งเสี่ยงต่อการล็อกข้อมูลแย่งกันและ scale ยาก Vernon ถึงกับเรียก large-cluster aggregate ว่าเป็น anti-pattern ที่ “จะไม่ perform หรือ scale ได้ดีเลย”
- อ้างอิง aggregate อื่นด้วย identity เท่านั้น — เก็บแค่ id ไม่เก็บ object reference ตรง เพื่อป้องกันการแก้ไขหลาย aggregate ในธุรกรรมเดียวโดยไม่ตั้งใจ
- ใช้ eventual consistency นอกขอบเขต — เมื่อ business process ต้องข้าม aggregate ให้ใช้ Domain Events แทนการทำธุรกรรมเดียวครอบคลุมหลาย aggregate
Microsoft Learn (Azure Architecture Center) ยืนยันแนวทางเดียวกัน: aggregate กำหนด consistency boundary รอบ entity หนึ่งตัวหรือมากกว่า และ “สิ่งที่ทำให้เป็น aggregate คือขอบเขตธุรกรรม (transactional boundary) ไม่ใช่จำนวน entity” — แม้ aggregate ที่มี entity เดียวไม่มีลูกเลยก็ยังนับเป็น aggregate ได้ เอกสารเดียวกันยังแนะนำว่าใน microservices แต่ละ service ไม่ควรเล็กกว่า1 aggregate และไม่ควรใหญ่กว่าหนึ่ง Bounded Context
ในทางปฏิบัติ aggregate root ยังรับผิดชอบรับประกัน ความสอดคล้อง (consistency) ของทั้งกลุ่มด้วยการไม่เปิดเผยลูกของมันตรง ๆ แต่ควบคุมการเข้าถึงเอง — เป็นตัวอย่างที่ชัดของ Single Point of Enforcement และ repository ควรมีความสัมพันธ์แบบ 1:1 กับ aggregate root เท่านั้น ไม่ใช่กับ entity ลูกภายใน
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”ตัวอย่างคลาสสิกคือ Order ที่มี OrderLine เป็นลูก โดย Order เป็น aggregate root ที่ควบคุมการเพิ่ม/ลบรายการ และตรวจ invariant เช่น ยอดรวมต้องไม่ติดลบ หรือ order ที่ถูก submit แล้วห้ามแก้ไขอีก
// Aggregate root — จุดเข้าเพียงจุดเดียวจากภายนอกpublic class Order{ private readonly List<OrderLine> _lines = new(); public Guid Id { get; } public OrderStatus Status { get; private set; } public IReadOnlyCollection<OrderLine> Lines => _lines.AsReadOnly();
public Order(Guid id) { Id = id; Status = OrderStatus.Draft; }
// ทางเดียวที่จะเพิ่มรายการได้ — ผ่าน root เท่านั้น public void AddLine(Guid productId, int quantity, decimal unitPrice) { if (Status != OrderStatus.Draft) throw new InvalidOperationException("แก้ไข order ที่ยืนยันแล้วไม่ได้"); if (quantity <= 0) throw new ArgumentException("จำนวนต้องมากกว่าศูนย์");
// ผูกความสัมพันธ์กับ Product ด้วย identity เท่านั้น (Vernon rule 3) _lines.Add(new OrderLine(productId, quantity, unitPrice)); }
public decimal Total => _lines.Sum(l => l.Quantity * l.UnitPrice);
public void Submit() { if (_lines.Count == 0) throw new InvalidOperationException("order ว่างเปล่าส่งไม่ได้");
Status = OrderStatus.Submitted; // แจ้ง aggregate อื่นแบบ eventual consistency (Vernon rule 4) DomainEvents.Raise(new OrderSubmitted(Id, Total)); }}
// entity ลูกภายใน aggregate — ไม่มี repository ของตัวเองpublic class OrderLine{ public Guid ProductId { get; } public int Quantity { get; } public decimal UnitPrice { get; }
internal OrderLine(Guid productId, int quantity, decimal unitPrice) { ProductId = productId; Quantity = quantity; UnitPrice = unitPrice; }}
public enum OrderStatus { Draft, Submitted }ข้อสังเกตสำคัญจาก code: OrderLine ไม่มี constructor แบบ public ที่เรียกจากภายนอกได้ (ใช้ internal แทน) และ Order ไม่เปิด List<OrderLine> ตรง ๆ แต่ห่อด้วย IReadOnlyCollection — ทั้งสองจุดคือกลไกที่ทำให้ root เป็น single point of enforcement จริง ๆ ไม่ใช่แค่ชื่อ
ในระดับ diagram ความสัมพันธ์ระหว่าง aggregate กับส่วนประกอบต่าง ๆ เป็นดังนี้:
classDiagram
class Order {
Guid Id
OrderStatus Status
AddLine
Submit
}
class OrderLine {
Guid ProductId
int Quantity
decimal UnitPrice
}
class OrderRepository {
Save
GetById
}
class Product {
Guid Id
}
class OrderSubmitted {
Guid OrderId
decimal Total
}
Order o-- OrderLine : aggregate root ถือ
OrderRepository ..> Order : จัดการทั้งก้อน
Order ..> Product : อ้างด้วย ProductId เท่านั้น
Order --> OrderSubmitted : raise เมื่อ submit
จาก diagram: OrderRepository คุยกับ Order (root) เท่านั้น ไม่มี OrderLineRepository แยกต่างหาก ส่วนความสัมพันธ์กับ Product ซึ่งเป็น aggregate อีกตัวหนึ่ง เชื่อมกันด้วย id ไม่ใช่ object reference ตรง
ความสัมพันธ์กับแนวคิดอื่น
หัวข้อที่มีชื่อว่า “ความสัมพันธ์กับแนวคิดอื่น”- Entity vs Aggregate — Entity คือหน่วยที่มี identity เดี่ยว ส่วน aggregate คือกลุ่มของ entity (บางครั้งมีแค่ entity เดียว) ที่ถูกมองเป็นหน่วยเดียวเพื่อวัตถุประสงค์ด้าน consistency — aggregate ทุกตัวมี root เป็น entity แต่ entity ทุกตัวไม่จำเป็นต้องเป็น aggregate root
- Value Object — ภายใน aggregate มักมี Value Object ประกอบอยู่ด้วย (เช่น
Money,Address) เพราะ value object ไม่มี identity จึงไม่จำเป็นต้องมี repository ของตัวเองและถูกจัดการโดย root เสมอ - Repository — Repository Pattern ควรมีขอบเขตตรงกับ aggregate root เสมอ คือ1 repository ต่อหนึ่งชนิด aggregate root ไม่ใช่ต่อ entity ลูก
- Domain Events — เมื่อ business process ต้องข้ามขอบเขต aggregate (rule ข้อ 4 ของ Vernon) กลไกที่ใช้เชื่อมมักเป็น Domain Events แทนธุรกรรมข้าม aggregate
- Bounded Context — Bounded Context เป็นขอบเขตที่ใหญ่กว่า อาจมีหลาย aggregate อยู่ภายใน ขณะที่ aggregate คือขอบเขตความสอดคล้องเชิงธุรกรรมที่เล็กกว่าและแคบกว่ามาก
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Entity
- Value Object
- Bounded Context
- Single Point of Enforcement
- Repository Pattern
- Persistence Ignorance
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/domain-driven-design/aggregate-pattern
- Domain-Driven Design — Eric Evans
- bliki: DDD_Aggregate — Martin Fowler
- Effective Aggregate Design — Vaughn Vernon (dddcommunity.org)
- Use Tactical DDD to Design Microservices — Azure Architecture Center, Microsoft Learn
- Designing a DDD-oriented microservice — .NET, Microsoft Learn