Aggregates — ขอบเขตของความถูกต้อง
โครงสร้างของ domain modelDomain ModelModel ของ Domain ที่ฝังกฎและกระบวนการทางธุรกิจไว้ใน code จริง หัวใจของ DDD คือการพัฒนา domain model ที่ “เข้าใจธุรกิจอย่างลึกซึ้ง”Strategic Design ประกอบด้วย EntityEntityอ็อบเจ็กต์ที่มี “ตัวตน” (identity) ต่อเนื่องตามเวลา แม้ค่าภายในเปลี่ยน ก็ยังเป็นสิ่งเดิม เช่น ลูกค้า, คำสั่งซื้อ — แยกแยะด้วย id ไม่ใช่ด้วยค่าTactical Design และ Value ObjectValue Objectอ็อบเจ็กต์ที่สำคัญที่ “ค่า” ของมัน ไม่มีตัวตนเฉพาะ สองตัวที่ค่าเท่ากันถือว่าเท่ากัน ควรเป็น immutable (เปลี่ยนค่าไม่ได้) เช่น เงิน, วันที่, พิกัดTactical Design ที่แทนแนวคิดต่างๆ ใน problem domain แต่สิ่งที่ทำให้ model ซับซ้อนจนสับสนเวลา implement จริง ไม่ใช่ตัว object เอง — มันคือ จำนวนและชนิดของความสัมพันธ์ (relationship) ระหว่าง object เหล่านั้นต่างหาก
บทนี้พูดถึงสองเรื่องที่เกี่ยวพันกัน: (1) วิธีทำให้ความสัมพันธ์ระหว่าง domain object เรียบง่ายและสอดคล้องกับ domain invariantInvariantกฎทางธุรกิจที่ “ต้องเป็นจริงเสมอ” ภายใน Aggregate เช่น ยอดรวมคำสั่งซื้อต้องเท่ากับผลรวมของรายการสินค้า — ต้องถูกต้องภายใน transaction เดียวTactical Design และ (2) แนวคิดหลักของบท — AggregateAggregateกลุ่มของ Entity และ Value Object ที่ถูกมองเป็นหนึ่งหน่วยเดียวเพื่อรักษาความถูกต้องของข้อมูล มีขอบเขตชัดเจน และเป็นหน่วยของ transactionTactical Design ซึ่งเป็น consistency boundary ที่ตัด model ก้อนใหญ่ออกเป็นกลุ่ม (cluster) เล็กๆ ที่จัดการได้ง่ายขึ้น Aggregate คือ tactical pattern ที่ทรงพลังที่สุดตัวหนึ่ง แต่ก็เป็นหนึ่งในตัวที่ออกแบบให้ถูกต้องได้ยากที่สุดเช่นกัน
จัดการ object graph ที่ซับซ้อน
หัวข้อที่มีชื่อว่า “จัดการ object graph ที่ซับซ้อน”มือใหม่ DDD มักโฟกัสที่ entity และ value object แต่ไม่ใส่ใจความสัมพันธ์ระหว่างมัน ผลคือเผลอสร้างความสัมพันธ์ที่ “เลียนแบบชีวิตจริง” หรือแย่กว่านั้นคือ “เลียนแบบ data model” โดยไม่ถามว่าความสัมพันธ์นั้น ทำงานอะไรให้ invariant หรือเปล่า model แบบนี้บดบังแนวคิดสำคัญ ทำให้ทั้ง domain expert และนักพัฒนาสับสน และนำไปสร้างจริงได้ยาก
domain model ไม่ใช่สิ่งเดียวกับ data model จุดประสงค์ของ domain model คือ รองรับ invariant และ use case ไม่ใช่รองรับหน้าจอ UI หรือการสร้างรายงาน — ความสัมพันธ์ใดที่ไม่ได้ทำงานให้ invariant ก็ไม่ต้อง implement มัน อย่าจำลองชีวิตจริง
เลือกทิศทางเดินเดียว (single traversal direction)
หัวข้อที่มีชื่อว่า “เลือกทิศทางเดินเดียว (single traversal direction)”model ที่สร้างเพื่อสะท้อนความจริงมักเต็มไปด้วยความสัมพันธ์แบบ สองทาง (bidirectional) คือ object สองตัวอ้างถึงกันและกัน ดูตัวอย่างระบบจัดซื้อในรูปที่ 19-1: เราเดินจาก Supplier ไป PurchaseOrder ได้และเดินกลับได้, เดินจาก Supplier ไป Product ได้และกลับได้ — ORM ทำให้สร้างความสัมพันธ์แบบนี้ง่ายเกินไป จนเกิด object graph ที่ลึกและ performance ตก

จำเป็นไหมที่ต้องโหลด Product ทั้งหมดของ Supplier ทั้งที่เราแค่อยากแก้เบอร์ติดต่อของ supplier? วิธีลดความซับซ้อนคือ บีบให้ความสัมพันธ์เดินทางได้ทิศเดียว — เปลี่ยนจาก bidirectional ให้เป็น unidirectional เวลาออกแบบ ให้ถามเสมอว่า: ความสัมพันธ์นี้รองรับ พฤติกรรมทางธุรกิจ (business behavior) อะไร และ ใคร ต้องใช้มัน
Qualify ความสัมพันธ์ และเลือก ID แทน object reference
หัวข้อที่มีชื่อว่า “Qualify ความสัมพันธ์ และเลือก ID แทน object reference”ถ้าความสัมพันธ์เป็นแบบ one/many-to-many ให้ qualify มันเพื่อจำกัดจำนวน object ที่ต้องโหลด เช่น สัญญามือถือหนึ่งฉบับ (Contract) ที่เก็บสายโทร (Calls) ทั้งหมด ลูกค้าที่อยู่เดือนที่ 23 ของสัญญา 24 เดือนจะมีข้อมูลสายเป็นร้อยๆ รายการ — แต่ invariant เรื่องนาทีฟรีคงเหลือต้องการแค่ “สายของรอบบิลปัจจุบัน” เท่านั้น จึงโหลดเฉพาะรอบนั้นก็พอ (ทำที่ระดับ repository / database)
แนวทางที่ทรงพลังที่สุดคือ เก็บ ID แทน object reference แล้วใช้ RepositoryRepositoryตัวที่ทำให้เราเข้าถึง Aggregate ราวกับเป็น collection ในหน่วยความจำ ซ่อนรายละเอียดฐานข้อมูล มี1 repository ต่อ1 Aggregate RootTactical Design ใน Application ServiceApplication Serviceบริการที่ “ประสานงาน” use case — จัดการ transaction, ความปลอดภัย, การบันทึก, แปลง DTO — แต่ไม่มีกฎธุรกิจอยู่ในตัวมันเองTactical Design ไปดึง object ที่ use case ต้องการ ลองดูเคสคลาสสิก: ในชีวิตจริงลูกค้าหนึ่งคนมีหลายคำสั่งซื้อ แต่ในพื้นที่คำตอบ (solution space) ของ app ไม่มี invariant ใด ที่บังคับให้ object Customer ต้องถือ collection ของ Order ทั้งหมด — การสร้างความสัมพันธ์นี้เป็น object reference จึงเพิ่มความซับซ้อนโดยไม่จำเป็น
public void PlaceOrder(Guid customerId, IEnumerable<Product> products){ // ประสานงานใน application service: ดึงเฉพาะสิ่งที่ use case นี้ต้องการ var customer = _customerRepository.FindBy(customerId); var addressBook = _addressBookRepository.FindBy(customerId); var order = customer.CreateOrderFor(products); order.DispatchTo(addressBook.Default); // ใช้ค่าเริ่มต้น ไม่ต้องโหลด order เก่าทั้งหมด _orderRepository.Add(order);}
วิธีดูว่า object reference จำเป็นไหม ให้ถาม: ความสัมพันธ์นี้รองรับ invariant ของ use case หรือเปล่า? Order ต้องการแค่ Customer ID เพื่อทำตาม invariant ไม่ต้องมี reference ที่เดินไปหา object ได้ — กรณีอื่นๆ ทั้งหมด ให้เลือก ID + repository เพื่อลด coupling
Aggregate คืออะไร และมีไว้ทำไม
หัวข้อที่มีชื่อว่า “Aggregate คืออะไร และมีไว้ทำไม”การลดและจำกัดความสัมพันธ์ช่วยให้ model เรียบง่ายขึ้น แต่ยังเหลืออีกหนึ่งความต้องการ: การจัดกลุ่ม object ที่ทำงานด้วยกัน ให้เป็นหน่วยเดียวที่ระบบจัดการได้อย่างรวดเร็วและเชื่อถือได้ — นี่คือหน้าที่ของ AggregateAggregateกลุ่มของ Entity และ Value Object ที่ถูกมองเป็นหนึ่งหน่วยเดียวเพื่อรักษาความถูกต้องของข้อมูล มีขอบเขตชัดเจน และเป็นหน่วยของ transactionTactical Design ที่จัดกลุ่ม object รอบๆ invariant พร้อมทำหน้าที่เป็น consistency และ concurrency boundary ไปในตัว
Aggregate มอบประโยชน์สามอย่าง:
- ออกแบบรอบ domain invariant — invariant คือกฎที่ “ต้องเป็นจริงเสมอ” เช่น bid ที่ชนะการประมูลต้องถูกวางก่อนการประมูลปิด เมื่อ object ทุกตัวที่เกี่ยวกับ invariant อยู่ใน Aggregate เดียวกัน เราจึงเทียบและประสานสถานะของมันได้ในที่เดียว
- ยกระดับการ abstraction — เรียก order กับ order line รวมกันว่า “order” ก้อนเดียว ทำให้สื่อสารและคิดในภาพรวมได้ง่ายขึ้น
- เป็น consistency boundary — กำหนดว่าข้อมูลส่วนไหนต้องถูกต้องตรงกันภายใน transaction เดียว
invariant ที่เจอในบท Value ObjectValue Objectอ็อบเจ็กต์ที่สำคัญที่ “ค่า” ของมัน ไม่มีตัวตนเฉพาะ สองตัวที่ค่าเท่ากันถือว่าเท่ากัน ควรเป็น immutable (เปลี่ยนค่าไม่ได้) เช่น เงิน, วันที่, พิกัดTactical Design ใช้กับ entity เดี่ยวๆ แต่ invariant ในบทนี้มัก คร่อม object หลายตัว จึงต้องใช้ Aggregate มาล้อมไว้ด้วยกัน
consistency ภายใน vs eventual consistency ภายนอก
หัวข้อที่มีชื่อว่า “consistency ภายใน vs eventual consistency ภายนอก”ถ้าเราเอา transactional boundary ก้อนเดียวไปครอบทั้ง model ปัญหาคือใน collaborative domain การแก้ไขที่ไม่เกี่ยวข้องกันเลยกลับชนกัน จนเกิด locking ที่ database ดังรูปที่ 19-9: ผู้ใช้ A เพิ่มที่อยู่ให้ลูกค้า ขณะที่ผู้ใช้ B เปลี่ยนสถานะคำสั่งซื้อของลูกค้าคนเดียวกัน — ไม่มี invariant ใดที่ห้ามทั้งสองเกิดพร้อมกัน แต่ boundary ที่ใหญ่เกินไปกลับ block อีกฝ่ายไว้

ในทางกลับกัน ถ้าไม่มี boundary เลย ก็เปิดช่องให้ invariant พังได้ ทางออกคือจัด boundary ให้ ตรงกับ invariant: Customer กับ Order เป็นคนละ Aggregate เพราะไม่มี invariant ใดที่ผูกทั้งคู่เข้าด้วยกัน

เพราะ Aggregate ถูกบันทึกและดึงแบบ atomic เป็นก้อนเดียว กฎที่คร่อม2 Aggregate จึง ไม่ consistent ทันที แต่จะเป็น eventual consistencyEventual Consistencyความถูกต้องที่ “ตามมาทีหลัง” ไม่ใช่ทันที ใช้กับการอัปเดตข้าม Aggregate ผ่าน Domain Event แทนการบังคับให้ตรงกันในทันทีภายใน transaction เดียวTactical Design แทน — Aggregate ที่ถูกอัปเดตรับผิดชอบแค่ความถูกต้องภายในตัวเอง บางครั้งจึงถือข้อมูลเก่า (stale) ที่ copy มาจาก Aggregate อื่น
ตัวอย่างนโยบายสะสมแต้ม: ถ้าลูกค้าใช้จ่ายเกิน $100 ในปีที่ผ่านมา จะได้ส่วนลด 10% ในการสั่งซื้อทุกครั้งถัดไป — Order กับ Loyalty เป็นคนละ Aggregate เมื่อสั่งซื้อ เราอัปเดต Order ใน transaction เดียว แล้ว publish Domain EventDomain Eventสิ่งที่ “เกิดขึ้นแล้ว” ใน domain ซึ่งส่วนอื่นสนใจ แทนด้วยอ็อบเจ็กต์ที่เปลี่ยนแปลงไม่ได้ ตั้งชื่อเป็นอดีต เช่น CargoWasRouted ใช้สื่อสารข้าม AggregateTactical Design OrderCreated ให้ Loyalty subscribe ไปอัปเดตทีหลังใน transaction ที่สอง (รูปที่ 19-14)

อย่าสับสนระหว่าง “save” กับ “load/create” — โหลดหลาย Aggregate ใน transaction เดียวได้ และสร้าง Aggregate ใหม่หลายตัวพร้อมกันก็ได้ (object ที่เพิ่งสร้างยังไม่ก่อ concurrency conflict) แต่การ save ควรเป็น Aggregate เดียวต่อ1 transaction เป็นค่าเริ่มต้นไว้ก่อนเสมอ — จะข้ามกฎนี้ก็ต่อเมื่อปรึกษากับธุรกิจแล้วว่าได้ประโยชน์ที่คุ้มกับความเสี่ยงจริงเท่านั้น
เลือกขนาด Aggregate ให้เล็กเข้าไว้
หัวข้อที่มีชื่อว่า “เลือกขนาด Aggregate ให้เล็กเข้าไว้”โดยทั่วไป Aggregate ที่เล็กกว่าทำให้ระบบเร็วและเชื่อถือได้กว่า เพราะถ่ายโอนข้อมูลน้อยลงและโอกาส concurrency conflict ต่ำลง ให้เริ่มจากเล็กก่อนแล้วค่อยๆ พิสูจน์ว่าทำไมต้องเพิ่มแต่ละแนวคิดเข้าไป ผลเสียของ Aggregate ใหญ่มีสามข้อ:
- performance ตก — ยิ่งสมาชิกเยอะ ยิ่งโหลด/บันทึกข้อมูลมาก โดยเฉพาะเมื่อ Aggregate คร่อมหลายตารางหรือหลาย document (เพิ่ม query/join)
- เสี่ยง concurrency conflict — Aggregate ใหญ่มักมีหลายหน้าที่ เกี่ยวข้องกับหลาย use case โอกาสที่ผู้ใช้หลายคนแก้พร้อมกันจึงสูง
- scale ยาก — การคร่อมหลายตารางคือ coupling ที่ระดับ database ทำให้ย้าย/แบ่ง partition ข้อมูลลำบาก (เทรนด์ polyglot persistence ต้องการ Aggregate ที่ decouple กันดี)
การทำ Aggregate ให้เล็กไม่ใช่กระสุนวิเศษ (silver bullet) ของ performance คุณยังต้องวัด round-trip และ database performance ของจริง เพื่อเข้าใจพฤติกรรมการเข้าถึงข้อมูลอย่างแท้จริง และต้อง ซื่อตรงต่อ domain ด้วย — ขนาดเป็นเพียงปัจจัยหนึ่ง ไม่ใช่เกณฑ์เดียวของการออกแบบที่ดี
นิยามขอบเขตของ Aggregate: กรณีศึกษา eBidder
หัวข้อที่มีชื่อว่า “นิยามขอบเขตของ Aggregate: กรณีศึกษา eBidder”หนังสือใช้เว็บประมูลออนไลน์ชื่อ eBidder (คล้าย eBay) ใน Bounded Context ชื่อ Listing เพื่อสาธิตหลักการ — มี object เช่น Listing, Auction, WinningBid, Bid, Question, Answer, Member, Seller, Category, ShippingMethod, PaymentMethod, Watches กฎสำคัญบางข้อ:
- listing ต้องอยู่ในอย่างน้อย1 category และเสนออย่างน้อยหนึ่งวิธีจ่ายเงินและหนึ่งวิธีจัดส่ง
- listing ขายผ่าน auction ซึ่งมีวันเริ่ม–สิ้นสุด และติดตาม winning bid
- auction ต้อง active และ winning bid ปัจจุบันต้องน้อยกว่าจำนวนที่จะ bid ใหม่
วิเคราะห์ตาม invariant: Question/Answer ไม่มี invariant ผูกกับ Listing (แค่อ้างผ่าน ID) จึงเป็น Aggregate ของตัวเอง; Auction กับ WinningBid มี invariant ร่วมกัน (ราคาปัจจุบันนิยามโดย winning bid) จึงอยู่ Aggregate เดียวกัน; Bid เป็นเหตุการณ์ในอดีต ไม่ผูก invariant ใด เป็น Aggregate เดี่ยว; Member, Seller, Watches ก็แยกเป็น Aggregate ของตัวเอง
Listing HAS Question และ HAS Auction แต่ไม่ได้แปลว่าต้องอยู่ Aggregate เดียวกัน — HAS-A เป็นเรื่องของ data model ไม่ใช่ของ invariant การลาก collection ของ bid เข้ามาใน auction “เพราะ auction มี bid” คือกับดักที่ทำให้ object graph บวมและได้ Aggregate ที่ไร้ประโยชน์ จง พิสูจน์การจัดกลุ่มทุกครั้งด้วย invariant ไม่ใช่ด้วยความเกี่ยวข้องกัน
จงออกแบบ Aggregate จากมุมของ business use case ไม่ใช่จากชีวิตจริง — ถามว่า invariant อะไรต้องเป็นจริงเพื่อทำ use case นั้นให้สำเร็จ ยกตัวอย่าง use case การ bid (PlaceBidFor) ไม่ต้องใช้ข้อมูล category หรือคำบรรยายของ listing เลย นี่คือเหตุผลที่ listing กับ auction เป็นคนละ Aggregate
public void Bid(Guid auctionId, Guid memberId, decimal amount){ var member = _memberService.GetMember(memberId); if (member.CanBid) { var auction = _auctions.FindBy(auctionId); // โหลดแค่ Auction Aggregate var offer = new Offer(memberId, new Money(amount), _clock.Time()); auction.PlaceBidFor(offer, _clock.Time()); // พฤติกรรมอยู่ที่ root }}Aggregate Root — ประตูเดียวเข้าสู่กลุ่ม
หัวข้อที่มีชื่อว่า “Aggregate Root — ประตูเดียวเข้าสู่กลุ่ม”เพื่อให้ Aggregate consistent เสมอ ส่วนประกอบภายในต้องไม่ถูก share ออกไปทั่ว model หรือเปิดให้ service layer แตะตรงๆ ทางออกคือเลือก entity หนึ่งตัวเป็น Aggregate RootAggregate RootEntity หนึ่งตัวที่เป็น “ประตูเดียว” เข้าสู่ Aggregate การอ้างอิงจากภายนอกทำได้กับ root เท่านั้น root จึงควบคุมกฎความถูกต้องของทั้งกลุ่มTactical Design — การสื่อสารกับ Aggregate ทั้งหมดต้องผ่าน root เท่านั้น

ลองเทียบให้เห็นความต่าง: ถ้าไม่มี root ผู้เรียกจะเข้าถึงโครงสร้างภายในและ ข้าม พฤติกรรมไปแก้สมาชิกตรงๆ ได้ — ทำลาย invariant ทันที:
// ฝ่าฝืน invariant ที่ว่า แต่ละ item ห้ามเกิน 10 ชิ้นbasket.Items.Find(x => x.ProductId == productId).Quantity = newQuantity;// ภายใน ChangeQuantityOf มี logic บังคับ invariant (เพดาน 10 ชิ้น)basket.ChangeQuantityOf(productId, newQuantity);Aggregate Root จึงควร เปิดเฉพาะ behavioral interface (method ที่สื่อแนวคิด domain เช่น ReduceTheStartingPrice(), CanPlaceBid(), PlaceBidFor()) และซ่อน reference ภายในไว้เป็น private — หลีกเลี่ยง getter/setter ที่เปิดสถานะภายในออกมา เพราะจะทำให้ object อื่นไปผูกติดกับโครงสร้างภายในและขัดขวางการ refactor model ในภายหลัง
Aggregate Root มี global identity เพราะถูกอ้างจากนอก Aggregate ได้ ส่วนสมาชิกอื่นมีแค่ local identity เพราะอยู่ภายในและมีความหมายเฉพาะในบริบทของ root เท่านั้น
กฎการอ้างอิงระหว่าง Aggregate
หัวข้อที่มีชื่อว่า “กฎการอ้างอิงระหว่าง Aggregate”- อ้าง Aggregate Root อื่นด้วย ID เสมอ ไม่ใช่ด้วย object reference — มิฉะนั้นการโหลด Aggregate หนึ่งอาจลากอีกหลายตัวตามมา (และเกิดปัญหา lazy loading ที่ debug ยาก) เช่น
Auctionเก็บแค่ItemId(ID ของListing) ไม่ใช่ objectListing - ห้ามสิ่งที่อยู่นอก Aggregate ถือ reference ไปยังสมาชิกภายใน — ผู้ใช้ Aggregate ถือได้แค่ reference ของ root เท่านั้น (รูปที่ 19-24)
- root ส่ง transient reference / สำเนา (copy) ของ object ภายในออกไปใช้ชั่วคราวใน method เดียวได้ แต่การแก้ไขใดๆ ต้องวิ่งกลับมาผ่าน root
- ตรงข้ามกับสัญชาตญาณเล็กน้อย: object ที่ ไม่ใช่ root ภายใน Aggregate หนึ่ง ถือ reference (ในรูปของ ID) ไปยัง root ของ Aggregate อื่นได้ เช่น
WinningBidเก็บ ID ของMemberที่ bid

ในระบบขนส่ง CargoAggregate RootEntity หนึ่งตัวที่เป็น “ประตูเดียว” เข้าสู่ Aggregate การอ้างอิงจากภายนอกทำได้กับ root เท่านั้น root จึงควบคุมกฎความถูกต้องของทั้งกลุ่มTactical Design ถือ Route SpecificationRoute SpecificationValue Object ที่ระบุ “ความต้องการของลูกค้า”: ต้นทาง ปลายทาง และกำหนดเวลาถึง — มี method isSatisfiedBy() ตรวจว่าแผนเดินทางตรงตามนี้หรือไม่Tactical Design และ ItineraryItineraryValue Object ที่เป็น “แผนการเดินทางจริง” ของ Cargo ประกอบด้วยลำดับของ Leg (ช่วงการขนส่งแต่ละช่วง)Tactical Design (value object) ไว้ภายใน เพราะ invariant “แผนเดินทางต้องตอบสนองความต้องการของลูกค้า” (isSatisfiedBy) ต้องเทียบทั้งสองในที่เดียว — จึงเป็น Aggregate เดียวกัน
แต่ Handling EventHandling Eventการบันทึกการจัดการสินค้าจริง (LOAD, UNLOAD, RECEIVE, CLAIM) เป็น Aggregate “แยกต่างหาก” จาก Cargo เพราะมีปริมาณมากและต้องประมวลผลแบบ asyncTactical Design (บันทึกการ LOAD/UNLOAD จริงที่ท่าเรือ) ถูกแยกเป็น คนละ Aggregate เพราะมันคือเหตุการณ์ในอดีตที่มีปริมาณมหาศาลและประมวลผลแบบ async — มันอ้าง Cargo ผ่าน Tracking IDTracking IDรหัสติดตามที่ไม่ซ้ำของ Cargo ในตัวอย่างการขนส่ง ใช้เป็น identity ของ Cargo ตั้งแต่ตอนจองจนส่งมอบTactical Design ไม่ใช่ผ่าน object reference ซึ่งเป็นรูปแบบเดียวกับเคส Bid ใน eBidder พอดี
Order (พร้อม OrderLine ภายใน) เป็น Aggregate หนึ่ง โดยมี invariant “ยอดรวมต้องเท่ากับผลรวมของ line” บังคับใน transaction เดียว ส่วนแต้มสะสม (Loyalty) เป็นอีก Aggregate — เมื่อสั่งซื้อสำเร็จ Order publish OrderCreated แล้ว handler ค่อยไปอัปเดต Loyalty ใน transaction ถัดไป
ลูกค้ายินดีรอให้ส่วนลด 10% มีผลใน “วันทำการถัดไป” ได้ (โดยมี SLA กำกับ) จึงไม่จำเป็นต้อง consistent ทันที — แลกกับการที่เราไม่ต้อง save object จำนวนมากรวมกันใน transaction เดียว
implement: persistence, transaction และ concurrency
หัวข้อที่มีชื่อว่า “implement: persistence, transaction และ concurrency”Persistence — มีเพียง Aggregate Root เท่านั้นที่ query จาก database ได้ตรงๆ object ภายในเข้าถึงผ่าน root เสมอ และมี หนึ่ง RepositoryRepositoryตัวที่ทำให้เราเข้าถึง Aggregate ราวกับเป็น collection ในหน่วยความจำ ซ่อนรายละเอียดฐานข้อมูล มี1 repository ต่อ1 Aggregate RootTactical Design ต่อ1 Aggregate Root ที่บันทึก/ดึง ทั้ง Aggregate เป็นหน่วยเดียว (รูปที่ 19-12) การลบ root ต้องลบ object ลูกทั้งหมดใน transaction เดียวกัน เพราะ order line ไม่มีความหมายถ้าไม่มี order

สำหรับ business use case ให้ดึง “ทั้ง Aggregate” เท่านั้น แต่สำหรับ การทำรายงาน (reporting) ไม่ต้องสน Aggregate — query ตรงที่ database ไปเติม thin view model ได้เลย
ระวัง lazy loading: มันทำให้เกิดปัญหา select n+1 เช่น Booking ที่มีแขก 50 คน วน loop ส่งอีเมล อาจยิง query ถึง 51 ครั้ง (Martin Fowler เขียนเรื่องนี้ไว้ละเอียด) ถ้า Aggregate เล็กพออยู่แล้ว ก็แทบไม่ต้องพึ่ง lazy loading เลย
Transactional consistency ภายใน — Aggregate ต้อง commit หรือ roll back ทั้งก้อนใน transaction เดียว ภายในขอบเขตนี้ object อ้างถึงกันด้วย direct reference ได้ (เช่น Auction ถือ WinningBid) จึงเห็นค่าล่าสุดของกันทันที นอก ขอบเขตให้ใช้ eventual consistency แทน — โดยทั่วไป implement ด้วย asynchronous domain event ผ่าน messaging (เช่น NServiceBus, Kafka)
public void PlaceOrder(Guid customerId, IEnumerable<Product> products){ var customer = _customerRepository.FindBy(customerId); var order = customer.AddOrder(products); _bus.Publish(new OrderCreated(order)); // publish เป็นส่วนหนึ่งของ transaction // commit _unitOfWork.SaveChanges();}Concurrency — root ปกป้อง invariant จากการ bypass ได้ แต่กันผู้ใช้หลายคนแก้พร้อมกันไม่ได้ มีสองแบบ: optimistic (สมมติว่าไม่ชน, “last change wins”) และ pessimistic (ต้องการให้ตรงกับตอนดึงมา) วิธีที่นิยมคือใส่ Version ไว้ที่ root แล้วเทียบเลข version กับใน database ก่อน commit ถ้าไม่ตรงให้โยน exception
public void Save(Loyalty loyalty){ var currentVersion = GetCurrentVersionOf(loyalty); if (currentVersion != loyalty.Version) throw new OptimisticConcurrencyException(/* ... */); // เลข version ตรงกัน — บันทึกต่อได้}Aggregate = consistency boundary ที่จัดกลุ่ม domain object รอบ invariant ไม่ใช่รอบ HAS-A หรือ UI; Aggregate Root คือประตูเดียวที่บังคับ invariant และเป็นตัวเดียวที่มี global identity; อ้างถึง Aggregate อื่นด้วย ID เสมอ, บันทึก 1 Aggregate ต่อ1 transaction ผ่าน repository, และใช้ eventual consistency ผ่าน domain event กับกฎที่คร่อมหลาย Aggregate
เจาะลึกแนวคิดในบทนี้ต่อได้ที่คลังอ้างอิง DevIQ:
- Aggregate — นิยามและหลักการออกแบบ Aggregate โดยตรง ต่อยอดจากเรื่อง consistency boundary ที่บทนี้อธิบาย
- Entity — Aggregate Root คือ Entity ที่ถูกเลือกให้เป็นประตูเดียวเข้าสู่กลุ่ม และมี global identity ตามที่บทนี้กล่าวถึง
- Repository — กติกา “1 Repository ต่อ1 Aggregate Root” ที่ใช้บันทึก/ดึงทั้ง Aggregate เป็นหน่วยเดียว
- Domain Events — กลไกหลักที่ใช้ทำ eventual consistency ระหว่าง Aggregate เช่นเคส
OrderCreatedที่อัปเดตLoyalty - Unit of Work — ประสาน transaction ตอน commit ให้ Aggregate ถูก save เป็นก้อนเดียวตามที่บทนี้เน้นย้ำ
เช็กความเข้าใจ — บทที่ 19
ข้อ 1 / 4เกณฑ์ที่ ‘พื้นฐานที่สุด’ ในการตัดสินว่า object กลุ่มไหนควรเป็น Aggregate เดียวกันคืออะไร?