Queries: รายงานจาก Domain
ซอฟต์แวร์ถูกสร้างขึ้นเพื่อรับใช้ความต้องการของธุรกิจ — ไม่ใช่แค่ feature ที่ “ทำเงิน” เท่านั้น แต่รวมถึงความสามารถในการ ประเมินว่าธุรกิจไปได้ดีแค่ไหน ด้วย นี่คือหน้าที่ของ “รายงาน” (report): ติดตามตัวเลขสำคัญและ KPI อย่างยอดขาย เป้าหมายทางการเงิน หรือความพึงพอใจของลูกค้า เหมือนกับที่ตลอดทั้งหนังสือเล่มนี้ได้แสดงให้เห็นว่า DDD มีหลายวิธีในการสร้างระบบ การทำรายงานก็มีหลายวิธีเช่นกัน
บทนี้พาคุณสำรวจตัวเลือกหลายๆ แบบในการ “อ่านข้อมูลออกมาแสดงผล” (query side) พร้อม trade-off ที่มากับแต่ละแบบ ตั้งแต่วิธีที่เร็วที่สุดในการขึ้นหน้ารายงาน ไปจนถึงสถาปัตยกรรมที่รวมข้อมูลจากหลายๆ Bounded ContextBounded Contextขอบเขตที่ชัดเจน (มักเป็นระบบย่อยหรือทีมหนึ่ง) ที่ model หนึ่งใช้ได้และมีความหมายแน่นอน ภายในขอบเขตนี้ Ubiquitous Language สอดคล้องกัน 100%Strategic Design เข้าด้วยกัน เป้าหมายคือให้คุณ คุ้นเคยกับ trade-off ของแต่ละแบบ เพื่อให้ตัดสินใจได้อย่างมีวิจารณญาณใน project จริง
ในบริบทของบทนี้ “รายงาน” คือ การนำเสนอชุดข้อมูลที่สอดคล้องกันเพื่อจุดประสงค์เชิงวิเคราะห์ ไม่จำกัดเฉพาะรายงานทางการสำหรับผู้บริหารหรือ BI เท่านั้น — สรุปโปร file Twitter ที่โชว์จำนวน follower / tweet / retweet ก็ถือเป็น “รายงาน” ตามนิยามกว้างๆ นี้ พูดง่ายๆ คือ ฝั่งอ่านข้อมูล (query) ทั้งหมดที่เอาไปแสดงให้ผู้ใช้เห็น
รายงานภายใน Bounded Context เดียว
หัวข้อที่มีชื่อว่า “รายงานภายใน Bounded Context เดียว”สถานการณ์ที่ง่ายที่สุดคือเมื่อข้อมูลทั้งหมดที่ต้องใช้ อยู่ใน Bounded Context เดียวกัน ถ้าเป็นเช่นนั้น ยังถือเป็นสัญญาณที่ดีว่าคุณ ลากเส้นขอบของ context ได้ถูกต้อง ด้วย เพราะรายงานมักออกแบบมาเพื่อแผนกใดแผนกหนึ่งในธุรกิจ ซึ่งควรจะ map ลงไปยัง Bounded Context ใด context หนึ่งได้พอดี
เมื่อไม่ต้องกังวลเรื่องการกระจายข้อมูล (distribution) คำถามใหญ่ที่สุดคือ: จะใช้ code domain ของเราในการสร้างรายงานหรือไม่?
ทางเลือกที่ 1 — ดึงรายงานจาก Domain Object
หัวข้อที่มีชื่อว่า “ทางเลือกที่ 1 — ดึงรายงานจาก Domain Object”ถ้าต้องการขึ้นหน้ารายงานให้เร็ว วิธีแรกที่ควรพิจารณาคือ เอา domain object ที่มีอยู่แล้วมา map ลง View Model (model ที่เตรียมข้อมูลและตรรกะการแสดงผลให้ view) วิธีนี้ใช้แรงน้อยที่สุด เพราะ reuse code เดิม
trade-off ใหญ่คือ ประสิทธิภาพ เพราะเราควบคุม query ที่ยิงเข้า datastore ได้น้อย ถ้าประสิทธิภาพไม่ใช่ข้อจำกัดคอขวด แต่ความเร็วในการพัฒนาสำคัญกว่า — นี่คือตัวเลือกที่ลงตัวที่สุด
ลองนึกถึง Application ServiceApplication Serviceบริการที่ “ประสานงาน” use case — จัดการ transaction, ความปลอดภัย, การบันทึก, แปลง DTO — แต่ไม่มีกฎธุรกิจอยู่ในตัวมันเองTactical Design ที่วน loop ดึงข้อมูลแต่ละหน่วยมาประกอบเป็นรายการในรายงาน:
public DealershipPerformanceReport BuildReport( IEnumerable<int> dealershipIds, DateTime start, DateTime end){ var statuses = new List<DealershipPerformanceStatus>(); foreach (var id in dealershipIds) { // select N+1 — reuse code domain เดิม (เขียนเร็ว) // แต่เสี่ยงยิง query หลายรอบ (ประสิทธิภาพแย่) var dealership = repository.Get(id); var targets = provider.Get(dealership, start, end); var actuals = calculator.CalculateFor(dealership, start, end);
// map จาก domain ลง view model เพื่อไม่ให้ UI ผูกกับ domain object statuses.Add(new DealershipPerformanceStatus { DealershipName = dealership.Name, TotalRevenue = actuals.TotalRevenue, TargetRevenue = targets.TargetRevenue, NetProfit = actuals.NetProfit, TargetProfit = targets.TargetProfit, }); } return new DealershipPerformanceReport { ReportStartDate = start, ReportEndDate = end, Dealerships = statuses };}code ข้างบนสวยและเขียนเร็ว แต่มีราคาแฝง — ทุก ID หนึ่งตัว เราเรียกฐานข้อมูลหลายครั้ง (dealership + targets + actuals) ถ้า ORM มี lazy-loading ด้วยแล้ว ระบบที่มี 10 dealership อาจกลายเป็น 30 query ซึ่งภายใต้ภาระโหลดสูงสุด (peak load) อาจล่มได้ ประสิทธิภาพไม่ได้สำคัญเสมอไป แต่ในงานรายงานแบบนี้ คุณควร รู้ตัวว่ากำลังจ่ายต้นทุนอะไรไป
ปัญหาอีกข้อของการ map คือ บางครั้งเราต้อง เปิด property เพิ่ม บน domain object ที่จริงๆ อยากเก็บไว้เป็น private/internal ของ domain ผลคือ service layer เริ่มผูกกับโครงสร้างภายในของ domain มากขึ้น เพื่อลด coupling ที่ไม่พึงประสงค์นี้ เราจึงมองหา pattern อื่น เช่น mediator
ร้านกีฬาออนไลน์อยากรู้ว่า “โปรแกรมสะสมแต้ม” สร้างกำไรได้ดีแค่ไหน รายงานแสดงเป็นรายเดือน: กำไรสุทธิ (% ของกำไรทั้งหมด), จำนวนการซื้อ, อัตราแต้มต่อดอลลาร์ และจำนวนสมาชิกที่สมัครใหม่
ในเฟสแรก ถ้าทุกอย่างอยู่ใน Bounded Context “การสั่งซื้อ” อยู่แล้ว เราก็ map จาก domain object มาเป็น view model ได้เลย — รวดเร็วทันใจ แต่เมื่อข้อมูลเติบโตขึ้น จำนวน query จะกลายเป็นปัญหา จนเราต้องขยับไปทางเลือกถัดๆ ไป
ทางเลือกที่ 2 — Mediator Pattern (ซ่อนโครงสร้าง domain)
หัวข้อที่มีชื่อว่า “ทางเลือกที่ 2 — Mediator Pattern (ซ่อนโครงสร้าง domain)”ถ้าอยากได้ view model ที่มีข้อมูลครบ แต่ ไม่ผูกกับโครงสร้างภายในของ domain มากเกินไป เราใช้ Mediator Pattern ได้ แทนที่จะให้ service layer ไป “อ่าน property” ของ domain เราจะ ส่ง view model ผ่านตัวกลาง (mediator) เข้าไปใน domain แล้วให้ domain object เป็นฝ่ายเติมค่าลงใน mediator เอง
ที่สำคัญ วิธีนี้ ไม่ทำลายการแบ่งชั้น (layering) เพราะ mediator implement interface ที่ “เป็นของ domain” เอง:
// interface mediator — โครงสร้างที่ “เสถียร” พอจะเปิดให้ service layer เห็นpublic interface IDealershipAssessment { int TotalRevenue { get; set; } int NetProfit { get; set; }}
public class DealershipPerformanceActuals { // field เป็น private — ซ่อนโครงสร้าง domain ที่อาจเปลี่ยนได้ private int totalRevenue; private int netProfit;
public void Populate(IDealershipAssessment mediator) { mediator.TotalRevenue = totalRevenue; mediator.NetProfit = netProfit; }}ข้อดีคือ field ที่เป็น private เหล่านั้น ไม่ถูกเปิดออกนอก domain จึงมีอิสระที่จะเปลี่ยนแปลงในอนาคต — ตรงข้ามกับการ map ตรงๆ ที่ทำให้โครงสร้างภายในรั่วไหลออกมา ส่วน mediator ตัวจริงจะห่อ view model ไว้: เมื่อ domain เซ็ตค่าลง mediator, mediator ก็เซ็ตค่าต่อลง view model ที่มันห่ออยู่
เลือก mediator เมื่อคุณ ต้องการ share state ที่เป็น private ของ domain โดยไม่เปิดโครงสร้างภายในออกมา แต่ถ้า domain ยังเติบโตไม่นิ่งและความซับซ้อนของ mediator ยังไม่จำเป็น — นั่นคือการออกแบบเกินเหตุ (over-engineering) และในรายงานที่เน้นประสิทธิภาพสูง mediator ก็ไม่เหมาะ เพราะควบคุมในระดับล่างไม่ได้ กรณีแบบนั้นให้ลงไปยิง query ตรงกับฐานข้อมูลแทน
ทางเลือกที่ 3 — ยิง Query ตรงเข้า Datastore
หัวข้อที่มีชื่อว่า “ทางเลือกที่ 3 — ยิง Query ตรงเข้า Datastore”เมื่อ ประสิทธิภาพและความคุ้มค่าเป็นเรื่องสำคัญ หรือเมื่อไม่ต้องการผ่านชั้น mapping ที่ซับซ้อน นัก DDD จำนวนมากเลือก ดึงข้อมูลรายงานตรงจากฐานข้อมูล โดยไม่แตะ domain เลย
- ระบบที่ใช้ CQRSCQRSCommand Query Responsibility Segregation — แยก model สำหรับ “เขียน/แก้ไข” (command) ออกจาก model สำหรับ “อ่าน” (query) เหมาะกับ domain ซับซ้อน แต่เพิ่มความซับซ้อน ใช้อย่างระมัดระวังArchitecture อยู่แล้วจะมีสำเนาข้อมูลแบบ denormalized เตรียมไว้ให้แต่ละรายงานโดยเฉพาะ
- ระบบที่ไม่ใช้ CQRS มักยิง query ดิบๆ ด้วยเทคโนโลยีระดับล่าง เช่น raw ADO.NET หรือใช้ feature ระดับล่างของ ORM (เช่น HQL ของ NHibernate)
แนวคิดเบื้องหลังคือ Micro-ORM อย่าง Dapper: มันไม่พยายาม “ซ่อน” ฐานข้อมูลเหมือน Big-ORM แต่แค่ทำให้ query สะดวกขึ้นนิดหน่อย — คุณยังคุม SQL ได้เต็มที่:
// คุม SQL เองทุกบรรทัด → เขียน query ที่ join หลายตารางได้อย่างมีประสิทธิภาพvar profits = con.Query<PurchasesAndProfit>(profitQuery, new { start, end });// (SQL จริงจะ join orders, users, loyaltyAccounts ฯลฯ เพื่อคำนวณ % กำไร)ข้อท้าทายใหญ่ของการ query ตรงคือ ความซ้ำซ้อนของแนวคิด (knowledge duplication) สังเกตว่าใน SQL เราคำนวณ “% กำไรจาก loyalty เทียบกับกำไรรวม” — การคำนวณแบบนี้ น่าจะมีอยู่แล้วใน domain model ด้วย เมื่อสูตรเปลี่ยน คุณต้องแก้ทั้งใน SQL และใน domain ซึ่งหลงลืมได้ง่ายมาก จนทำให้ธุรกิจได้ตัวเลขที่ผิดพลาด
ทางบรรเทาคือ “เก็บค่าที่คำนวณไว้ล่วงหน้า” ลงฐานข้อมูลตอนอัปเดต แต่ถ้าอัปเดตจากหลายที่ ก็ต้องคำนวณซ้ำในหลายที่ หรือใช้ database trigger — ซึ่งก็มี trade-off ของมันเองอีก (อ่านยากและทดสอบยาก)
ทางเลือกที่ 4 — อ่านจาก Denormalized View Cache
หัวข้อที่มีชื่อว่า “ทางเลือกที่ 4 — อ่านจาก Denormalized View Cache”บางครั้งแม้แต่ query ตรงด้วย SQL ที่เขียนมือ ก็ยังช้าเกินไป นัก DDD บางคนจึงสร้าง สำเนาข้อมูลแบบ denormalized ที่เตรียมไว้เฉพาะรายงาน เรียกว่า Denormalized View Cache แนวคิดนี้ใกล้เคียงกับ CQRS มาก: ทุกครั้งที่มีการอัปเดต ข้อมูลหลักถูกอัปเดต และ view cache ที่เกี่ยวข้องก็ถูกอัปเดตไปพร้อมกันผ่านตัวที่เรียกว่า denormalizer

หน้าที่ของ denormalizer คือ แบนข้อมูล (flatten) เพื่อให้ query กลายเป็น SQL SELECT ธรรมดาที่ไม่ต้อง join เลย:
public LoyaltyReport Build(DateTime start, DateTime end) { // ไม่มี join — แค่ select จากตารางที่ denormalize ไว้แล้ว var query = "select [Month], PointsPerDollar, NetProfit, Signups, Purchases" + " from denormalizedLoyaltyReportViewCache" + " where [Month] >= @start and [Month] < @end"; var summaries = con.Query<LoyaltySummary>(query, new { start, end }); return new LoyaltyReport { Summaries = summaries };}Denormalized view cache คือการ ยอมลงทุนแรงในฝั่งเขียน (denormalize) เพื่อแลกกับ query ที่อ่านง่ายและเร็วมากๆ นี่คือปรัชญาเดียวกับ CQRS — แยกฝั่งเขียนออกจากฝั่งอ่าน คุณต้องชั่งน้ำหนักว่าความซับซ้อนที่ลดลงและประสิทธิภาพที่เพิ่มขึ้นนั้น คุ้มกับงาน denormalize ที่เพิ่มเข้ามาหรือไม่ และผสมผสานหลายวิธีใน project เดียวได้ตามความเหมาะสม
ทางเลือกที่ 5 — Projection จาก Event Stream
หัวข้อที่มีชื่อว่า “ทางเลือกที่ 5 — Projection จาก Event Stream”ระบบที่ใช้ Event SourcingEvent Sourcingเก็บทุกการเปลี่ยนสถานะเป็น event ที่เปลี่ยนไม่ได้ แล้วสร้างสถานะปัจจุบันด้วยการเล่น event ซ้ำ มัก “ต้องใช้คู่กับ CQRS” (Greg Young) มี event stream ต่อ1 AggregateArchitecture ต้องใช้เทคนิคที่แตกต่างออกไป เพราะมัน ไม่ได้เก็บสถานะปัจจุบัน ของ app แต่เก็บลำดับ event ทั้งหมด แทนที่จะ query ตาราง เราใช้ feature ที่เรียกว่า Projection — คือ “query บน event stream ที่ผลิต state ที่ต้องการ หรือผลิต stream ใหม่” โดยอิงจากเนื้อหาของ event ใน stream ต้นทาง
ลองดูตัวอย่างรายงานการวินิจฉัยโรครายเดือนของหน่วยงานสาธารณสุข: เราสร้าง stream สรุปรายเดือนของแต่ละการวินิจฉัยในรูปแบบ diagnosis-{id}-{month} จาก projection ที่ทำงานบน stream กลางที่มีทุกการวินิจฉัย (diagnoses):

projection ใน Event Store เขียนด้วย JavaScript และทำงานกับทุก event ใน stream:
fromStream('diagnoses') .whenAny(function (state, ev) { var date = ev.data.Date.replace('/', ''); var diagnosisId = ev.data.DiagnosisId; // linkTo สร้าง “ตัวชี้” ไปยัง stream อีกอัน(สร้าง stream ถ้ายังไม่มี) // projection ไม่ได้ copy event จริง — แค่สร้าง reference linkTo('diagnosis-' + diagnosisId + '_' + date, ev); });พอจัดกลุ่ม event เข้า stream รายเดือนแล้ว การ “นับจำนวน” ก็ทำด้วย projection อีกตัวที่วนทุก stream ในหมวด (fromCategory('diagnosis').foreachStream()) แล้วบวกทีละ 1 เก็บไว้ใน state ของ projection สุดท้าย การสร้างรายงานเหลือเพียง เรียก HTTP ไปถามค่า count ของแต่ละ stream แล้ว map:
// ดึงยอดรวมรายเดือนจาก state ของ projection (ตอบกลับเป็น JSON)var url = projectionStateUrl + "?partition=" + streamName; // เช่น month-201402var count = Json.Decode<DiagnosisCount>(new WebClient().DownloadString(url));สังเกตว่า projection ใช้แนวคิดเดียวกับ denormalized view cache — ลงแรงทำงานหนักล่วงหน้า เพื่อให้ตอนอ่านเหลือเพียง “นับจำนวน event ใน stream” การยิง HTTP ทีละเดือนต่อการวินิจฉัยอาจเกิดขึ้นจำนวนมาก แต่ค่าเหล่านี้ ไม่เปลี่ยนแปลงอีกแล้ว จึง cache ได้อย่างเต็มที่ และ Event Store ถูกออกแบบมาให้รองรับ stream จำนวนนับล้านได้อย่างสบาย
ระบบ Cargo เก็บ Handling EventHandling Eventการบันทึกการจัดการสินค้าจริง (LOAD, UNLOAD, RECEIVE, CLAIM) เป็น Aggregate “แยกต่างหาก” จาก Cargo เพราะมีปริมาณมากและต้องประมวลผลแบบ asyncTactical Design (LOAD, UNLOAD, RECEIVE, CLAIM) ไว้เป็นจำนวนมหาศาล ถ้าระบบนี้ใช้ event sourcing เราสามารถสร้าง projection ที่จัดกลุ่ม handling event ตาม “ท่าเรือต่อเดือน” แล้วนับจำนวน เพื่อทำรายงาน “ปริมาณงานของแต่ละท่าเรือ” โดยไม่ต้องไปรบกวน AggregateAggregateกลุ่มของ Entity และ Value Object ที่ถูกมองเป็นหนึ่งหน่วยเดียวเพื่อรักษาความถูกต้องของข้อมูล มีขอบเขตชัดเจน และเป็นหน่วยของ transactionTactical Design ฝั่งเขียนเลย — งานหนักทำตอน project, ตอนอ่านแค่ count
รายงานข้ามหลาย Bounded Context
หัวข้อที่มีชื่อว่า “รายงานข้ามหลาย Bounded Context”ความยุ่งยากเริ่มต้นขึ้นจริงเมื่อข้อมูลกระจายอยู่หลาย Bounded Context ในระบบกระจาย (distributed) ซึ่งแต่ละ context มี datastore ของตัวเองและ loosely coupled ต่อกัน การจะรวมข้อมูลมาทำรายงานจึงต้องใช้แรงเพิ่ม หนังสือเสนอไว้ 2 แนวทางหลัก
แนวทางเบา — Composed UI
หัวข้อที่มีชื่อว่า “แนวทางเบา — Composed UI”ถ้างานส่วนใหญ่ทำได้ เป็นเฟสๆ โดยแต่ละเฟสอยู่ใน context เดียว เราอาจรวมผลลัพธ์ที่ชั้น UI ได้เลย ลองนึกถึงรายงานเปรียบเทียบความนิยมของค่ายเพลงในแต่ละภูมิภาค โดย “ความนิยม” = ยอด stream + ยอด download รวมกัน:

ข้อดีคือ stream กับ download เป็นคนละ Bounded Context กันโดยสมบูรณ์ การรวมเกิดขึ้นเป็นเฟสอิสระ — ดึงยอด download จาก Downloads context พร้อมกับดึงยอด stream จาก Streaming context แล้วใช้ aggregation ฝั่ง client หรือ server มารวม (เทคนิค UI composition จากบทประกอบ app) ส่วนการแปลง ID เป็นชื่อก็ค่อยไปถาม context เจ้าของข้อมูลทีหลังได้
แนวทางหนัก — Reporting Bounded Context แยกต่างหาก
หัวข้อที่มีชื่อว่า “แนวทางหนัก — Reporting Bounded Context แยกต่างหาก”ด้วยเหตุผลด้านประสิทธิภาพ ความสะดวก หรือเพื่อทำ data warehouse (ให้ data scientist “หั่นและพลิก” (slice and dice) ข้อมูลเพื่อหามุมมองใหม่ๆ) บางครั้งเราต้องการให้ข้อมูลรายงาน ทั้งหมดอยู่ใน datastore เดียว โดยปกติทำไม่ได้เพราะแต่ละ context แยก datastore กัน — แต่เรารู้ว่า context สื่อสารกันด้วย event จึงเปิดโอกาสให้สร้าง Reporting Bounded Context ที่ subscribe event จากหลาย context แล้วเก็บข้อมูลที่ต้องใช้ไว้ในที่เดียว (อาศัยหลัก event-driven และ Integration EventIntegration Eventevent ที่สื่อสาร “ข้าม” Bounded Context หรือข้าม microservice ผ่าน event bus — ต่างจาก Domain Event ที่ทำงานภายใน context เดียว (in-process)Architecture)

ขอบเขตและความซับซ้อนของ reporting context มีได้ตั้งแต่เรียบง่ายไปจนถึงหนักหน่วง: กรณีง่ายที่สุดก็เพียง subscribe event แล้วเก็บลง SQL (รูปที่ 26-4) ส่วนปลายอีกด้านหนึ่งคือการป้อนข้อมูลผ่านเทคโนโลยีที่หลากหลาย — recommendation engine และ machine learning คล้ายกับที่ Netflix ทำ:

- Composed UI — เบา ใช้เมื่อรวมข้อมูลเป็นเฟสอิสระได้ ไม่ต้องเก็บสำเนาข้อมูล แต่ผูกกับการที่ทุก context พร้อมตอบ query
- Reporting BC แยก — หนักกว่า สร้างสำเนาข้อมูลในที่เดียวด้วยการ subscribe event เหมาะกับ data warehouse / การวิเคราะห์ข้ามแผนก แต่ต้องดูแล eventual consistency และ infrastructure เพิ่ม
สรุปประเด็นสำคัญ
หัวข้อที่มีชื่อว่า “สรุปประเด็นสำคัญ”- รายงานสร้างได้หลายวิธี โดยมีเป้าหมายร่วมกันคือ หลีกเลี่ยง coupling กับ domain
- บางรายงานอยู่ใน context เดียว ขณะที่บางรายงานต้อง query ข้ามหลาย context
- Map จาก domain object เป็นวิธีที่เร็วที่สุด แต่ควบคุม data access ในระดับล่างได้น้อย (ระวัง N+1 query)
- Mediator pattern ช่วยซ่อนโครงสร้างภายในของ domain และจัดการ trade-off เรื่อง coupling
- ยิง query ตรง ทำได้เมื่อต้องการประสิทธิภาพ แต่ต้องระวังการละเมิด DRY (ตรรกะซ้ำกับ domain)
- Denormalized view cache และ Projection ของ event stream ต่างก็ย้ายงานหนักไปไว้ “เบื้องหลัง/ฝั่งเขียน” เพื่อแลกกับการอ่านที่เรียบง่าย
- รายงานข้าม context ทำได้ด้วย UI composition ในบางกรณี และ reporting context แยกต่างหาก ในอีกบางกรณี
ไม่มี “วิธีที่ถูกต้องที่สุด” มีแต่ trade-off — ความเร็วในการพัฒนา vs ประสิทธิภาพ vs การดูแลรักษา vs ความซ้ำซ้อน หน้าที่ของคุณคือ รู้ให้ชัดว่ากำลังแลกอะไรกับอะไร แล้วเลือกให้เหมาะกับบริบทของรายงานนั้นๆ และจำไว้ว่าคุณผสมผสานหลายวิธีในระบบเดียวได้เสมอ
เจาะลึกแนวคิดในบทนี้ต่อได้ที่คลังอ้างอิง DevIQ:
- CQRS — ปรัชญาแยกฝั่งเขียน/อ่านที่ denormalized view cache และ projection ของ event sourcing ในบทนี้ยึดตามอยู่
- Domain Events — กลไกเบื้องหลัง Reporting Bounded Context ที่ subscribe event จากหลาย context มาสร้างรายงานรวม
- Bounded Context — หน่วยขอบเขตที่กำหนดว่ารายงานหนึ่งจะอยู่ใน context เดียวหรือต้องรวมข้ามหลาย context
- Lazy Loading — ตัวการที่ทำให้การ map จาก domain object กลายเป็นกับดัก N+1 query อย่างที่บทนี้เตือนไว้
เช็กความเข้าใจ — บทที่ 26
ข้อ 1 / 4trade-off หลักของการ ‘map จาก domain object ลง view model’ เพื่อทำรายงานคืออะไร?