Domain Events
นักปฏิบัติ DDD ค้นพบว่า การจะเข้าใจ problem domain ได้ลึกขึ้น เราไม่ควรมองแค่ “สิ่งของ” (entity) เท่านั้น แต่ต้องมองด้วยว่า มี “เหตุการณ์” อะไรเกิดขึ้นบ้างใน domain เหตุการณ์เหล่านี้คือ Domain EventDomain Eventสิ่งที่ “เกิดขึ้นแล้ว” ใน domain ซึ่งส่วนอื่นสนใจ แทนด้วยอ็อบเจ็กต์ที่เปลี่ยนแปลงไม่ได้ ตั้งชื่อเป็นอดีต เช่น CargoWasRouted ใช้สื่อสารข้าม AggregateTactical Design — สิ่งที่เกิดขึ้นแล้วและธุรกิจให้ความสำคัญ มันมักถูกค้นพบในช่วง knowledge crunching ร่วมกับ domain expert และมีคุณค่ามากจนเกิดเทคนิคเวิร์กช็อปอย่าง EventStormingEventStormingเวิร์กช็อปแบบร่วมมือ (Alberto Brandolini, 2013) ใช้กระดาษโน้ตสีติดบนผนัง: ส้ม=event, น้ำเงิน=command, เหลือง=aggregate ฯลฯ เพื่อค้นพบ model และขอบเขตของ contextProcess ที่มุ่งสำรวจ event โดยตรง
แต่เมื่อ model เชิงแนวคิดกลายเป็น “event-centric” code ก็ต้องเป็น event-centric ตามไปด้วย เพื่อสะท้อน model นั้น และนี่คือจุดที่ domain events design pattern เข้ามาเพิ่มคุณค่า — บทนี้ว่าด้วยการนำ domain event มาแสดงออกใน code ทางเลือกในการ implement และ trade-off ของแต่ละแบบ
คำว่า “domain event” หมายถึงได้สองอย่าง: (1) เหตุการณ์ที่เกิดใน problem domain จริงๆ ซึ่งเป็นส่วนหนึ่งของ Ubiquitous LanguageUbiquitous Languageภาษากลางที่ทุกคนในทีม (ทั้งนักพัฒนาและผู้เชี่ยวชาญธุรกิจ) ใช้ร่วมกัน อิงกับ domain model โดยตรง คำเดียวกันต้องหมายถึงสิ่งเดียวกัน และสะท้อนลงใน codeStrategic Design และ (2) design pattern ที่ใช้จำลองเหตุการณ์นั้นในซอฟต์แวร์ ในบทนี้ส่วนใหญ่เราหมายถึง design pattern ยกเว้นจะระบุเป็นอย่างอื่น
เข้าใจผิดกันบ่อยว่าใช้ domain event แล้วต้องใช้ Event SourcingEvent Sourcingเก็บทุกการเปลี่ยนสถานะเป็น event ที่เปลี่ยนไม่ได้ แล้วสร้างสถานะปัจจุบันด้วยการเล่น event ซ้ำ มัก “ต้องใช้คู่กับ CQRS” (Greg Young) มี event stream ต่อ1 AggregateArchitecture — ซึ่งไม่จริง คุณยังใช้ฐานข้อมูล SQL แบบเดิมได้ตามปกติ แม้ว่า domain event จะจับคู่เข้ากับ event sourcing หรือ asynchronous messaging ได้ลงตัวก็ตาม (รายละเอียดอยู่ในบท Event Sourcing)
แก่นของ domain events pattern
หัวข้อที่มีชื่อว่า “แก่นของ domain events pattern”ถ้าคุณรู้จัก publish/subscribe หรือ event ในภาษาอย่าง C# คุณจะเข้าใจ pattern นี้ได้เร็วมาก หัวใจคือ: เราใช้คอมโพเนนต์ตัวกลาง (บางครั้งเรียกจากภายใน domain model เอง) เพื่อ publish event ออกไป จากนั้น event handler ทุกตัวที่ลงทะเบียนกับ event ชนิดนั้นจะถูกเรียกทำงาน โดยค่าเริ่มต้นจะทำงานแบบ synchronous อยู่บน thread เดียวกัน — แต่จะทำแบบ asynchronous ก็ได้เช่นกัน
event คือ “เหตุการณ์สำคัญที่เกิดขึ้นแล้ว”
หัวข้อที่มีชื่อว่า “event คือ “เหตุการณ์สำคัญที่เกิดขึ้นแล้ว””ตัว event เองเป็นแค่ class ธรรมดาที่ เปลี่ยนแปลงไม่ได้ (immutableImmutableเปลี่ยนแปลงไม่ได้หลังสร้าง ถ้าจะ “เปลี่ยนค่า” ต้องสร้างอ็อบเจ็กต์ใหม่แทน หลักการสำคัญของ Value Object เพื่อเลี่ยง bug จากการใช้อ้างอิงร่วมกันTactical Design) มี property แบบ public เป็น data object (POCO/POJO) ที่แทนเหตุการณ์สำคัญใน domain ตั้งชื่อเป็น อดีตกาล เสมอ เพราะมันคือสิ่งที่ “เกิดไปแล้ว”
// event ภายในที่เกิดเมื่อจัดส่งช้ากว่าที่รับประกันไว้// ภายใน bounded context เดียว แนบ domain object ทั้งก้อนได้เลยclass DeliveryGuaranteeFailed { constructor(public readonly order: OrderForDelivery) {}}handler คือสิ่งที่ “ตอบสนอง” ต่อ event
หัวข้อที่มีชื่อว่า “handler คือสิ่งที่ “ตอบสนอง” ต่อ event”เมื่อ event ถูก raise ขึ้น event handler ที่ลงทะเบียนไว้จะถูกเรียก handler เป็นได้ทั้ง class หรือ callback/lambda แบบ anonymous ก็ได้ — ตัวอย่างเช่น ลงทะเบียน method onDeliveryFailure ให้ถูกเรียกเมื่อ DeliveryGuaranteeFailed ถูก raise
function confirm(deliveredAt: Date, orderId: Guid) { // ลงทะเบียน handler ชั่วคราวเฉพาะช่วง use case นี้ using(DomainEvents.register(DeliveryGuaranteeFailed, onDeliveryFailure), () => { const order = orderRepository.findBy(orderId); order.confirmReceipt(deliveredAt); });}domain events คือการเพิ่ม ความหมายเชิง domain ลงบน publish/subscribe ทั่วไป — แยก “สิ่งที่เกิดขึ้น” (event) ออกจาก “สิ่งที่ต้องทำต่อ” (handler) อย่างเด็ดขาด เพื่อให้ domain model เปล่งเสียงเหตุการณ์จริงของธุรกิจออกมาได้ชัด โดยไม่ผูกตัวเองเข้ากับ side effect เชิงเทคนิค
asynchrony ที่เลือกได้ และขอบเขตของ transaction
หัวข้อที่มีชื่อว่า “asynchrony ที่เลือกได้ และขอบเขตของ transaction”domain events เข้ากันได้ดีกับ workflow แบบ asynchronous รวมถึงการสื่อสารข้าม bounded context (อย่างที่เราเห็นในเรื่อง integration ของ Part II) คุณสามารถ trigger กระบวนการเหล่านี้จากภายใน handler ได้ — บางครั้งคุณอาจต้องการ asynchronous communication ที่เชื่อถือได้แม้แต่ภายใน context เดียว เช่นเวลาทำ aggregate ที่เป็น eventually consistentEventual Consistencyความถูกต้องที่ “ตามมาทีหลัง” ไม่ใช่ทันที ใช้กับการอัปเดตข้าม Aggregate ผ่าน Domain Event แทนการบังคับให้ตรงกันในทันทีภายใน transaction เดียวTactical Design
แต่จุดที่ต้องระวังที่สุดคือ ขอบเขตของ transaction: ถ้า handler ตัวหนึ่งอัปเดตฐานข้อมูล และอีกตัวส่งข้อความเข้า queue คุณต้องการให้ทั้งคู่ rollback พร้อมกัน หากตัวใดตัวหนึ่งล้มเหลว มิฉะนั้นข้อมูลจะไม่สอดคล้องกัน

ถ้า handler สองตัวต้องสำเร็จหรือล้มเหลว “พร้อมกันแบบ atomic” คุณต้องบังคับให้มันทำงานแบบ synchronous การรีบ publish แบบ asynchronous โดยไม่คำนึงถึงขอบเขตของ transaction จะเปิดช่องให้เกิด bug จากความไม่สอดคล้องของข้อมูล (data inconsistency)
internal event กับ external event ต่างกันอย่างไร
หัวข้อที่มีชื่อว่า “internal event กับ external event ต่างกันอย่างไร”นี่คือเส้นแบ่งสำคัญที่ถ้าพลาดจะนำไปสู่การ implement ที่แย่ได้
- Internal event อยู่ “ภายใน” domain model เดียว ไม่ share ข้าม bounded context เพราะมันถูกจำกัดขอบเขต จึง แนบ domain object ทั้งก้อนไปกับมันได้ (อย่างในตัวอย่างแรก) ไม่เสี่ยงอะไร เพราะ context อื่นไม่มีทางมาผูกติดกับ object เหล่านั้น และถ้าคุณแก้ event แบบ breaking change code จะ compile ไม่ผ่านทันที (ในภาษาที่ compile) จึง ไม่ต้องทำ versioning
- External event สื่อสารข้าม context — นี่คือ Integration EventIntegration Eventevent ที่สื่อสาร “ข้าม” Bounded Context หรือข้าม microservice ผ่าน event bus — ต่างจาก Domain Event ที่ทำงานภายใน context เดียว (in-process)Architecture ที่เราพูดถึงใน Part II มันต้อง แบนราบ (flat) เปิดเผยแค่ property ไม่กี่ตัว ส่วนมากเป็นเพียง correlation id และต้องทำ versioning เพื่อเลี่ยง breaking change
// external event เปิดเผยได้แค่ id ห้ามแนบ domain object ทั้งก้อนclass DeliveryGuaranteeFailed { constructor(public readonly orderId: Guid) {}}ใน use case หนึ่งๆ มักมี internal event เกิดขึ้นหลายตัว แต่มี external event แค่หนึ่งหรือสองตัวที่ถูก raise โดย service layer ดังรูปที่ 18-2 — จึงควรแยก event ทั้งสองชนิดไว้คนละ namespace ให้ชัด

ในระบบขนส่ง เมื่อวาง ItineraryItineraryValue Object ที่เป็น “แผนการเดินทางจริง” ของ Cargo ประกอบด้วยลำดับของ Leg (ช่วงการขนส่งแต่ละช่วง)Tactical Design ใหม่ให้ Cargo สำเร็จ Aggregate จะ raise internal event ชื่อ CargoWasRouted ภายใน Booking context — handler ภายในอาจไปอัปเดตสถานะการติดตาม ส่วน service layer จะแปลงมันเป็น external event แบนๆ ที่มีแค่ trackingId แล้ว publish ออกไปให้ context อื่น (เช่น การแจ้งเตือนลูกค้า) นี่คือ internal vs external ในรูปที่ 18-2 พอดี
handler ใน domain ต่างจาก handler ใน service layer
หัวข้อที่มีชื่อว่า “handler ใน domain ต่างจาก handler ใน service layer”ถึงหน้าตาจะเหมือนกัน แต่ความรับผิดชอบต่างกันมาก ต้องแยกให้ออก
handler ใน domain — เรียก domain logic
หัวข้อที่มีชื่อว่า “handler ใน domain — เรียก domain logic”handler ที่อยู่ใน domain model จัดการ event ที่เกิดในนั้น มันจำลอง “ลำดับการโต้ตอบ” ที่เกิดใน problem domain จริงๆ เช่น ตัว validator ตรวจสอบคำสั่งซื้อว่าลูกค้าไม่ติด blacklist จากการค้างชำระครั้งก่อน แล้ว raise event ของตัวเองเพื่อ trigger ขั้นถัดไปของกระบวนการธุรกิจ (business process) — เป็นเรื่องปกติที่ domain event handler จะ delegate ไปยัง Domain ServiceDomain Serviceบริการที่ถือ “ตรรกะธุรกิจ” ซึ่งไม่เข้ากับ Entity หรือ Value Object ตัวใดตัวหนึ่งโดยธรรมชาติ เช่น การโอนเงินที่เกี่ยวข้องกับสองบัญชี — ไร้สถานะ (stateless)Tactical Design
handler ใน service layer — เรียก application/infrastructure logic
หัวข้อที่มีชื่อว่า “handler ใน service layer — เรียก application/infrastructure logic”handler อีกกลุ่มอยู่ที่ service layerApplication Serviceบริการที่ “ประสานงาน” use case — จัดการ transaction, ความปลอดภัย, การบันทึก, แปลง DTO — แต่ไม่มีกฎธุรกิจอยู่ในตัวมันเองTactical Design มันทำงานเชิง infrastructure เช่น ส่งอีเมล หรือคุยกับ external service อย่าง payment gateway ไม่ใช่ส่วนหนึ่งของ UL หรือ domain ความรับผิดชอบสำคัญของมันคือการ trigger การสื่อสารกับ bounded context ภายนอก (กลายเป็น external event)
handler ที่ทำงานเชิงเทคนิค (คุยกับ database, ส่งอีเมล) ควรลงทะเบียนไว้ที่ service layer เพื่อกัน domain model ให้สะอาด หรือถ้าจำเป็น ให้ประกาศเป็น contract ของ Domain ServiceDomain Serviceบริการที่ถือ “ตรรกะธุรกิจ” ซึ่งไม่เข้ากับ Entity หรือ Value Object ตัวใดตัวหนึ่งโดยธรรมชาติ เช่น การโอนเงินที่เกี่ยวข้องกับสองบัญชี — ไร้สถานะ (stateless)Tactical Design แล้วไป implement จริงที่ service layer
ทางเลือกในการ implement (และ trade-off)
หัวข้อที่มีชื่อว่า “ทางเลือกในการ implement (และ trade-off)”domain events เป็น pattern ที่ยืดหยุ่นมาก — มีหลายวิธี implement ตั้งแต่ใช้ของพื้นฐานในภาษา ไปจนถึง message bus
1. ใช้ event ในตัวภาษา (เช่น event keyword ของ C#)
หัวข้อที่มีชื่อว่า “1. ใช้ event ในตัวภาษา (เช่น event keyword ของ C#)”วิธีเริ่มต้นที่ง่ายที่สุดคือพึ่ง native feature ของภาษา ลงทุนน้อย แต่มี ราคาที่ต้องจ่าย: เกิด tight coupling ระหว่างผู้ publish กับผู้ subscribe — handler ต้องรู้ว่า object ตัวไหนเป็นคน publish event
class OrderForDelivery { // ผู้ subscribe ต้องผูกกับ entity ตัวนี้โดยตรง = coupling แน่น deliveryGuaranteeFailed = new EventEmitter<DeliveryGuaranteeFailed>();
confirmReceipt(deliveredAt: Date) { if (this.status === OrderStep.Delivered) return; this.deliveredAt = deliveredAt; this.status = OrderStep.Delivered; if (this.offer.isNotSatisfiedBy(this.placedAt, deliveredAt)) { // ใน C# ต้องเช็ก null ก่อน raise เพราะ event ที่ไม่มี subscriber จะเป็น null this.deliveryGuaranteeFailed.raise(new DeliveryGuaranteeFailed(this)); } }}2. ใช้ in-memory bus (เช่น NServiceBus)
หัวข้อที่มีชื่อว่า “2. ใช้ in-memory bus (เช่น NServiceBus)”เพื่อ คลาย coupling ให้ใช้ in-memory message bus แทน — publish event ผ่าน bus และลงทะเบียน handler ผ่าน bus ผู้ subscribe ไม่จำเป็นต้องรู้ว่าใครเป็นผู้ publish (และกลับกัน) บาง bus รองรับทั้ง synchronous และ asynchronous ทำให้ปรับบางส่วนเป็น async ได้ง่ายเมื่อจำเป็น
ข้อยุ่งยากคือต้อง “ส่ง bus” ไปทั่ว domain model ซึ่งเป็น concern เชิงเทคนิคที่ในอุดมคติเราไม่อยากให้มันมารบกวน domain — แต่บางทีมก็ยอมรับ trade-off นี้ เพราะมันทำให้ model สื่อความหมายเชิง domain ได้ดีขึ้นโดยรวม
3. static DomainEvents class (แนวของ Udi Dahan)
หัวข้อที่มีชื่อว่า “3. static DomainEvents class (แนวของ Udi Dahan)”อีก version ยอดนิยมคือ static class ชื่อ DomainEvents ที่อยู่ใน domain model และจัดการ concern เชิงเทคนิคให้ มี2 method สำคัญคือ Register<T> และ Raise<T> — กระชับกว่า in-memory bus มาก
class DomainEvents { // ปัญหาใหญ่คือ threading: ทุก thread ใช้ instance เดียวกัน private static actions: Delegate[] = [];
static register<T>(callback: (e: T) => void): Disposable { this.actions.push(callback); // คืน Disposable เพื่อถอด handler อัตโนมัติหลังใช้เสร็จ return { dispose: () => remove(this.actions, callback) }; }
static raise<T>(event: T): void { for (const action of this.actions) action(event); }}เพราะ DomainEvents เป็น static ทุก thread จึงใช้ collection เดียวกัน — handler ที่ลงทะเบียนใน thread หนึ่งจะถูกเรียกเมื่อ thread อื่น raise event ด้วย ใน C# มีเทคนิคใช้ [ThreadStatic] ให้แต่ละ thread มี collection ของตัวเอง แต่ Scott Hanselman เตือนว่าห้ามใช้ ThreadStatic ใน app ASP.NET เด็ดขาด
4. เลี่ยง static ด้วย method injection
หัวข้อที่มีชื่อว่า “4. เลี่ยง static ด้วย method injection”ถ้าไม่อยากผูก domain เข้ากับ static publisher (ซึ่งนักปฏิบัติ DDD หลายคนก็ไม่อยาก) ให้ ส่ง dispatcher เข้าไปเป็น argument ของ method ใน domain แทน — แลกมากับ signature ของ method ที่ปนเปื้อน parameter เชิงเทคนิค จึงควรใช้อย่างระมัดระวัง
confirmReceipt(deliveredAt: Date, dispatcher: IEventDispatcher) { // ... if (this.offer.isNotSatisfiedBy(this.placedAt, deliveredAt)) { dispatcher.raise(new DeliveryGuaranteeFailed(this)); }}5. เก็บ event ไว้บน aggregate root แล้วค่อย dispatch
หัวข้อที่มีชื่อว่า “5. เก็บ event ไว้บน aggregate root แล้วค่อย dispatch”แนวที่นิยมที่สุดแนวหนึ่งคือ แยกการ publish ออกจากการ handle เพื่อกัน side effect ให้เป็นอิสระ ทำได้โดยเก็บ collection ของ event ไว้บน Aggregate RootAggregate RootEntity หนึ่งตัวที่เป็น “ประตูเดียว” เข้าสู่ Aggregate การอ้างอิงจากภายนอกทำได้กับ root เท่านั้น root จึงควบคุมกฎความถูกต้องของทั้งกลุ่มTactical Design (เช่น property recordedEvents) แล้วให้ service layer เป็นคนเรียก dispatcher มา publish หลัง aggregate ทำงานเสร็จ — เก็บ concern เชิงเทคนิคไว้นอก domain model collection นี้จะ ว่างเสมอ ตอนโหลดจาก persistence มันบรรจุเฉพาะ event ที่เกิดใน transaction ปัจจุบัน
class OrderForDelivery { // event ที่เกิดใน transaction นี้เท่านั้น recordedEvents: object[] = [];
confirmReceipt(deliveredAt: Date) { // ...แทนที่จะ publish ทันที ให้บันทึกไว้ก่อน this.recordedEvents.push(new DeliveryGuaranteeFailed(this)); }}
// service layer ดึง event ที่ถูกบันทึกไว้ไป dispatch หลัง use case จบfunction confirm(deliveredAt: Date, orderId: Guid) { const order = orderRepository.findBy(orderId); order.confirmReceipt(deliveredAt); for (const evnt of order.recordedEvents) dispatcher.dispatch(evnt);}ข้อดีคือ side effect จะไม่ “แทรก” เข้ามากลางคันระหว่างที่ method กำลังทำงาน — มันจะเกิดขึ้นทีหลัง เมื่อ control ไหลกลับมาที่ application service แล้วเท่านั้น
การเก็บ event ไว้บน aggregate root เป็นพื้นฐานสำคัญของการสร้าง event-sourced domain model — ถ้าคุณกำลังพิจารณา Event SourcingEvent Sourcingเก็บทุกการเปลี่ยนสถานะเป็น event ที่เปลี่ยนไม่ได้ แล้วสร้างสถานะปัจจุบันด้วยการเล่น event ซ้ำ มัก “ต้องใช้คู่กับ CQRS” (Greg Young) มี event stream ต่อ1 AggregateArchitecture แนวนี้เกี่ยวข้องโดยตรง คุณสามารถใช้ IoC container เป็น dispatcher ได้ด้วย (เช่น สแกน assembly แล้วลงทะเบียน handler อัตโนมัติตาม convention ผ่าน interface IHandles<T>)
ลองนึกถึง Order ใน e-commerce: เมื่อลูกค้าชำระเงินสำเร็จ Aggregate จะ recordedEvents.push(new OrderWasPaid(...)) แทนที่จะยิงอีเมลทันที จากนั้น application service จะ dispatch event นี้ออกไป — handler ใน domain อาจ delegate ไป Domain ServiceDomain Serviceบริการที่ถือ “ตรรกะธุรกิจ” ซึ่งไม่เข้ากับ Entity หรือ Value Object ตัวใดตัวหนึ่งโดยธรรมชาติ เช่น การโอนเงินที่เกี่ยวข้องกับสองบัญชี — ไร้สถานะ (stateless)Tactical Design เพื่อจองสต็อก ส่วน handler ใน service layer ส่งอีเมลใบเสร็จและ publish external event ให้ระบบจัดส่ง การ “เก็บแล้วค่อย dispatch” ทำให้กฎธุรกิจ (จองสต็อก) แยกจาก side effect เชิงเทคนิค (ส่งอีเมล) อย่างสะอาด
การ test domain events
หัวข้อที่มีชื่อว่า “การ test domain events”การ test code ที่ใช้ domain events ไม่ได้ยากกว่า code OO ทั่วไป บางครั้งง่ายกว่าด้วยซ้ำ เพราะไม่ต้อง mock collaborator แต่รูปแบบ test ต่างกันเล็กน้อย
- Unit test (แบบ publish): ลงทะเบียน callback ที่ตั้ง flag ไว้ แล้วเรียก domain behavior ถ้า flag เป็น
trueตอนจบ แปลว่า event ถูก raise จริง — การมี staticDomainEventsไม่ได้ทำให้ test ยาก เพราะเราไม่ต้อง mock มัน - Unit test (แบบ recorded): เรียก behavior แล้ว assert ว่า event ถูก บันทึก ลง
recordedEvents(ไม่ใช่ถูก publish) - Application service test: test ระดับ service layer แทบไม่ต่างจากตอนไม่ได้ใช้ domain events เลย เพราะ event และ handler เป็นแค่ implementation detail — เซ็ตอัป service เรียก use case แล้ว assert ว่า external event ที่คาดหวังถูก publish ออกไป
it("raise DeliveryGuaranteeFailed เมื่อส่งช้ากว่ารับประกัน", () => { let eventWasRaised = false; offer.stub(isNotSatisfiedBy).returns(true); const order = new OrderForDelivery(/* ... */, offer); using(DomainEvents.register(DeliveryGuaranteeFailed, () => { eventWasRaised = true; }), () => { order.confirmReceipt(timePizzaDelivered); }); expect(eventWasRaised).toBe(true);});เพราะ test ระดับ application service ไม่มีร่องรอยของ domain events pattern เลย คุณจึง refactor domain model ให้หันมาใช้ domain events ได้โดยไม่ต้องแตะ test service layer แม้แต่บรรทัดเดียว — ซ้ำ test ชุดเดิมยังทำหน้าที่เป็นตาข่ายนิรภัย คอยยืนยันว่าการ refactor ไม่ได้ทำพฤติกรรมเดิมพัง
สรุปประเด็นสำคัญ
หัวข้อที่มีชื่อว่า “สรุปประเด็นสำคัญ”- domain event คือเหตุการณ์สำคัญในโลกจริงของ problem domain และเป็นส่วนหนึ่งของ Ubiquitous LanguageUbiquitous Languageภาษากลางที่ทุกคนในทีม (ทั้งนักพัฒนาและผู้เชี่ยวชาญธุรกิจ) ใช้ร่วมกัน อิงกับ domain model โดยตรง คำเดียวกันต้องหมายถึงสิ่งเดียวกัน และสะท้อนลงใน codeStrategic Design
- domain events ยังเป็น design pattern ที่ทำให้เหตุการณ์เหล่านั้นปรากฏชัดเจนใน code ในลักษณะเดียวกับ publish/subscribe
- ต่างจาก messaging ข้าม bounded context — domain events ทำงาน ภายใน domain model เดียว และมักเป็น synchronous
- มันช่วยให้ค่อยๆ ย้ายบาง use case ไปเป็น asynchronous ได้อย่างราบรื่นเมื่อถึงเวลาจำเป็น
- handler บางตัวอยู่ใน domain (เรียก domain logic) บางตัวอยู่ที่ service layer (งานเชิง infrastructure)
- มีหลายวิธี implement: native event (coupling แน่น), in-memory bus (coupling หลวม), static
DomainEvents, method injection, และการเก็บ event ไว้บน aggregate root - การใช้ domain events ไม่ได้บังคับ ให้ใช้ event sourcing แม้หลายคนจะพบว่าจับคู่กันแล้วได้ผลดี
- สุดท้ายแล้ว pattern นี้คือเรื่องของ trade-off — แลกการเพิ่ม indirection หนึ่งชั้นและการกระจาย code ไปหลาย file กับ domain model ที่ห่อหุ้มสมบูรณ์และขับเน้นเหตุการณ์จริงของ domain
flowchart LR A["Domain Model<br/>(Aggregate)"] -->|raise| B["Domain Event<br/>(immutable, อดีตกาล)"] B --> C["Internal Handler<br/>(domain logic)"] B --> D["Service-layer Handler<br/>(infrastructure)"] D -->|publish| E["External Event<br/>(Integration Event)"] E -.->|messaging / HTTP| F["Consumer Context"]
เจาะลึกแนวคิดในบทนี้ต่อได้ที่คลังอ้างอิง DevIQ:
- Domain Events — บทอ้างอิงตรงตัวของ design pattern ที่บทนี้อธิบายทั้งหมด ตั้งแต่แนวคิด publish/subscribe ไปจนถึงทางเลือกการ implement
- Event Aggregator — pattern ที่อยู่เบื้องหลัง in-memory bus และ static
DomainEventsclass ที่ใช้คลาย coupling ระหว่างผู้ publish กับผู้ subscribe - Aggregate — ฐานของแนวทาง “เก็บ event บน aggregate root แล้วค่อย dispatch” ที่บทนี้แนะนำว่าเป็นแนวที่นิยมที่สุด
- Event Storming — เทคนิคเวิร์กช็อปที่ใช้ค้นพบ domain event ตั้งแต่ต้น ก่อนที่มันจะถูกนำมาแสดงออกเป็น code ตามที่บทนี้กล่าวถึง
เช็กความเข้าใจ — บทที่ 18
ข้อ 1 / 4ข้อใดอธิบาย 'domain events pattern' ได้ถูกต้องที่สุด?