จาก Event Storming สู่ Bounded Context และ code
ห้าบทที่ผ่านมาเราแปะ Post-it สีส้มเต็มผนัง เรียง Domain EventDomain Eventสิ่งที่ “เกิดขึ้นแล้ว” และมีความหมายต่อผู้เชี่ยวชาญธุรกิจ เขียนเป็นกริยาอดีต (เช่น OrderPlaced, PaymentCaptured) แทนด้วย Post-it สีส้ม เป็นหน่วยตั้งต้นของ EventStorming ทุกแบบTactical Design เป็นไทม์ไลน์ เติม CommandCommandความตั้งใจหรือการตัดสินใจที่ “สั่งให้เกิด” domain event เขียนเป็นกริยาคำสั่ง (เช่น PlaceOrder) แทนด้วย Post-it สีฟ้า มักมี actor เป็นผู้ลงมือTactical Design กับ ActorActorคน/บทบาทที่ออก command (เช่น ลูกค้า ร้านอาหาร ไรเดอร์) แทนด้วย Post-it สีเหลืองแผ่นเล็กแปะมุมของ command ช่วยตอบว่า “ใครเป็นคนทำ”Process เข้ามา หา AggregateAggregateกลุ่มของ object ที่ถือกฎความสอดคล้อง (invariant) ไว้ด้วยกัน ใน EventStorming คือ “สิ่งที่รับ command แล้วปล่อย event ออกมา” (เช่น Order, Payment) แทนด้วย Post-it สีเหลืองแผ่นใหญ่Tactical Design ที่รับคำสั่งแล้วปล่อย event ลาก PolicyPolicyกฎเชิงปฏิกิริยาแบบ “whenever X → do Y” ที่คอยฟัง event หนึ่งแล้วสั่ง command ต่อโดยอัตโนมัติ (เช่น เมื่อ PaymentCaptured → แจ้งร้านให้กดรับ) แทนด้วย Post-it สีม่วงอ่อนProcess เชื่อม event กับ command ถัดไป และแปะ HotspotHotspotจุดที่เป็นปัญหา ข้อขัดแย้ง หรือคำถามที่ยังตอบไม่ได้ระหว่างเวิร์กช็อป แทนด้วย Post-it สีแดงวางเฉียง ๆ เก็บไว้กลับมาคุยทีหลังโดยไม่ขัดจังหวะการไหลของกลุ่มProcess สีแดงตรงจุดที่ยังตอบไม่ได้ ตอนนี้ผนังเต็มไปด้วยความรู้ที่เมื่อก่อนกระจายอยู่ในหัวคนหลายฝ่าย คำถามของบทสุดท้ายคือ แล้วยังไงต่อ? จะเอากำแพง Post-it นี้ไปทำเป็น design และ code ที่รันได้อย่างไร โดยไม่ทำให้ความรู้ที่อุตส่าห์ขุดมาหล่นหายระหว่างทาง คำตอบสั้นๆ คือ Event Storming ไม่ใช่จุดจบ แต่เป็น สะพาน จากเวิร์กช็อปไปสู่ Bounded ContextBounded Contextขอบเขตที่ model และ ubiquitous language หนึ่งใช้ได้และมีความหมายแน่นอน ใน EventStorming มักค้นพบขอบเขตนี้จาก pivotal event และกลุ่ม event ที่เกาะกลุ่มกันStrategic Design และ code แบบ DDD
Pivotal Event คือเบาะแสของ Bounded Context
หัวข้อที่มีชื่อว่า “Pivotal Event คือเบาะแสของ Bounded Context”ย้อนกลับไปที่ Pivotal EventPivotal Eventdomain event สำคัญที่แบ่งไทม์ไลน์ออกเป็นช่วง ๆ (เช่น OrderPlaced, FoodDelivered) มักขีดเส้นแนวตั้งกำกับ ใช้เป็นเบาะแสของขอบเขต bounded context ในภายหลังProcess — event สำคัญที่เราขีดเส้นแนวตั้งกำกับไว้ (OrderPlaced, PaymentCaptured, FoodDelivered) มันไม่ได้แค่แบ่งไทม์ไลน์ให้อ่านง่ายเท่านั้น แต่มันคือ รอยร้าวตามธรรมชาติของ domain ลองสังเกตสิ่งที่เกิดขึ้นรอบๆ เส้นแนวตั้งพวกนี้ให้ดี: ก่อนเส้นกับหลังเส้น คนที่คุยเปลี่ยนหน้า คำศัพท์ที่ใช้เปลี่ยนชุด และกฎที่ต้องรักษาก็เปลี่ยนเรื่อง
ก่อน OrderPlaced เราพูดภาษาของ “ตะกร้า” — เพิ่มของ ลบของ คำนวณราคารวม หลัง PaymentCaptured เราพูดภาษาของ “การจัดส่ง” — ใครไปรับ เส้นทางไหน ถึงเมื่อไหร่ คำว่า “ออเดอร์” ในสองฝั่งนี้ก็ไม่เหมือนกันเป๊ะๆ ด้วยซ้ำ ฝั่งสั่งซื้อมองออเดอร์เป็น “รายการอาหารกับราคา” ส่วนฝั่งจัดส่งมองออเดอร์เป็น “พัสดุที่มีต้นทางปลายทาง” นี่แหละคือสัญญาณของ ภาษาที่เปลี่ยน ซึ่งเป็นหัวใจของ Bounded Context
เมื่อ event และ aggregate เกาะกลุ่มกันแน่นภายในช่วงหนึ่งของไทม์ไลน์ และภาษาเปลี่ยนตรงรอยต่อ นั่นคือเบาะแสว่าเราเจอ ขอบเขตหนึ่ง แล้ว
flowchart LR
subgraph ORD["Ordering Context"]
E1["ItemAddedToCart"]
E2["OrderPlaced"]
end
subgraph PAY["Payment Context"]
E3["PaymentCaptured"]
end
subgraph DIS["Dispatch / Delivery Context"]
E4["OrderAcceptedByRestaurant"]
E5["FoodPrepared"]
E6["RiderAssigned"]
E7["FoodPickedUp"]
E8["FoodDelivered"]
end
subgraph RAT["Ratings Context"]
E9["OrderRated"]
end
E1 --> E2 --> E3 --> E4 --> E5 --> E6 --> E7 --> E8 --> E9
classDef evt fill:#e8743b,stroke:#b45309,color:#1a1a1f;
class E1,E2,E3,E4,E5,E6,E7,E8,E9 evt;
สังเกตว่า pivotal event (OrderPlaced, PaymentCaptured, FoodDelivered) นั่งอยู่ตรงรอยต่อของกล่องพอดี มันเป็น event ที่ context หนึ่ง “ปล่อยของ” ส่งต่อให้ context ถัดไปรับช่วง เส้นแบ่ง context ไม่ได้ถูกกำหนดโดยสถาปนิกที่นั่งเทวดา แต่ โผล่ขึ้นมาเองจากไทม์ไลน์ ที่ทั้งทีมช่วยกันสร้าง
ฟู้ดเดลิเวอรีแบ่งเป็น 4 Bounded Context
หัวข้อที่มีชื่อว่า “ฟู้ดเดลิเวอรีแบ่งเป็น 4 Bounded Context”เอาเบาะแสจากไทม์ไลน์มาตัดจริง เราได้สี่ขอบเขตที่แต่ละอันมีภาษาและความรับผิดชอบของตัวเอง:
- Ordering — โลกของตะกร้าและการยืนยันคำสั่งซื้อ พูดภาษา
Cart,OrderLine,PlaceOrderจบหน้าที่ตรงที่ออเดอร์ถูกวางแล้ว - Payment — โลกของเงิน พูดภาษา
Charge,Refund,PaymentCapturedสนใจแค่ว่าเก็บเงินสำเร็จหรือไม่ ไม่แคร์ว่าในกล่องมีผัดกะเพราหรือข้าวมันไก่ - Dispatch / Delivery — โลกของการเคลื่อนของ พูดภาษา
Rider,Route,Pickup,Dropoffมองออเดอร์เป็นงานขนส่งชิ้นหนึ่ง - Ratings — โลกของฟีดแบ็ก พูดภาษา
Review,Score,Commentเกิดขึ้นหลังจบงานทุกอย่างแล้ว
คำว่า “ออเดอร์” ปรากฏในทั้งสี่ที่ แต่ไม่ได้แปลว่ามันต้องเป็น class เดียวกัน — ตรงกันข้าม การพยายามยัดออเดอร์เดียวให้ทุก context ใช้ร่วมกันคือทางลัดสู่ก้อนโคลนที่พันกันยุ่ง แต่ละ context ควรมี Order version ของตัวเองที่ถือเฉพาะข้อมูลและกฎที่ context นั้นสนใจ นี่คือหลัก Bounded Context ตรงๆ ที่คอร์ส DDD สอน แต่คราวนี้เรา ค้นพบมันจากผนัง Post-it แทนที่จะนั่งเดาบนกระดาษเปล่า
ในเวิร์กช็อปฟู้ดเดลิเวอรี ให้จับกลุ่ม event ที่ “พูดเรื่องเดียวกัน” ไว้ด้วยกัน แล้วมองหาจุดที่คำศัพท์เปลี่ยน — ItemAddedToCart กับ OrderPlaced อยู่ก๊วนเดียวกัน (Ordering) พอถึง PaymentCaptured ภาษาเปลี่ยนเป็นเรื่องเงิน (Payment) พอถึง RiderAssigned, FoodPickedUp ภาษาเปลี่ยนเป็นเรื่องขนส่ง (Dispatch) แต่ละก๊วนที่ภาษาต่างกันคือ Bounded Context หนึ่งอัน และ Ubiquitous LanguageUbiquitous Languageภาษากลางที่ทีมและผู้เชี่ยวชาญธุรกิจใช้ร่วมกัน EventStorming เป็นเครื่องมือชั้นดีในการ “ขุด” ภาษานี้ออกมา เพราะทุกคำบน Post-it ถูกเถียงและตกลงกันสด ๆStrategic Design ของแต่ละอันก็คือคำบน Post-it ในก๊วนนั้นเอง
Context Map และ ACL ที่รอยต่อ
หัวข้อที่มีชื่อว่า “Context Map และ ACL ที่รอยต่อ”รู้ว่ามี4 context แล้วยังไม่พอ ต้องรู้ด้วยว่ามัน คุยกันอย่างไร เครื่องมือนั้นชื่อ context map — แผนที่ระดับสูงที่บอกความสัมพันธ์และทิศทางการพึ่งพาระหว่าง context โดยไม่ลงรายละเอียดข้างใน
flowchart LR ORD["Ordering"] PAY["Payment"] DIS["Dispatch / Delivery"] RAT["Ratings"] PG["Payment Gateway"] ORD -->|"OrderPlaced (integration event)"| PAY PAY -->|"PaymentCaptured"| DIS DIS -->|"FoodDelivered"| RAT PAY <-->|"ACL"| PG classDef ctx fill:#fef3c7,stroke:#b45309,color:#1a1a1f; classDef ext fill:#f9a8d4,stroke:#9d174d,color:#1a1a1f; class ORD,PAY,DIS,RAT ctx; class PG ext;
ลูกศรระหว่างกล่องเหลือง (context ของเรา) ส่วนใหญ่คือ integration event — event ที่ข้ามขอบเขตเพื่อบอก context ถัดไปว่า “ตาคุณแล้ว” OrderPlaced วิ่งจาก Ordering ไป Payment PaymentCaptured วิ่งต่อไป Dispatch ที่น่าสนใจคือกล่องสีชมพู Payment Gateway — มันคือ External SystemExternal Systemระบบภายนอกที่เราไม่ได้ควบคุมแต่ต้องคุยด้วย (เช่น Payment Gateway, บริการแจ้งเตือน, แผนที่) แทนด้วย Post-it สีชมพู ช่วยชี้จุดเชื่อมต่อและความเสี่ยงArchitecture ที่เรา ไม่ได้เป็นเจ้าของ เราสั่งมันไม่ได้ เปลี่ยน API มันไม่ได้ และมันก็พูดภาษาของมันเอง (transaction_id, auth_code, สารพัด field ที่เราไม่ได้ตั้งชื่อ)
ตรงรอยต่อกับระบบภายนอกนี่แหละที่ต้องมี ACL (Anti-Corruption Layer) — ชั้นแปลภาษาที่กันไม่ให้ศัพท์และโครงสร้างของระบบภายนอกไหลเข้ามาปนเปื้อน model ข้างใน Payment context ของเรา ACL รับ response ดิบๆ จาก gateway แล้วแปลเป็น PaymentCaptured ที่พูดภาษาของเรา ถ้าวันหนึ่งเราเปลี่ยนเจ้า gateway code ที่ต้องแก้ก็จำกัดอยู่แค่ใน ACL ไม่ลามเข้า domain นี่คือเหตุผลที่ Hotspot แดงๆ อย่าง “ยกเลิกหลังจ่ายเงินแล้ว คืนเงินยังไง?” มักซ่อนความซับซ้อนของ ACL เอาไว้ — เพราะการคืนเงินต้องเต้นตามจังหวะของระบบภายนอก ไม่ใช่ของเรา
ทุกครั้งที่เจอ Post-it สีชมพู (External System) ติดกับรอยต่อ context ให้ตีวงตรงนั้นว่าเป็นตำแหน่งของ ACL ใน code Payment Gateway, Push Notification, Map/Routing ล้วนต้องมีชั้นแปลของตัวเอง มันไม่ใช่งานเสริม — มันคือปราการที่ทำให้ Ubiquitous Language ภายใน context สะอาดอยู่ได้ทั้งที่ต้องคุยกับโลกภายนอกที่เราคุมไม่ได้
จาก Aggregate สู่ code
หัวข้อที่มีชื่อว่า “จาก Aggregate สู่ code”ทีนี้ซูมเข้าไปใน1 context แล้วแปลง Post-it เป็น class กฎการแปลจำง่ายมาก:
- Aggregate (เหลืองใหญ่) → class ที่ถือ invariant ของตัวเอง
- Command (ฟ้า) → method บน aggregate นั้น
- Domain Event (ส้ม) → สิ่งที่ method นั้น publish ออกมา หลังจากกฎผ่านแล้ว
flowchart LR A["ลูกค้า (Actor)"] --> C["PlaceOrder (Command)"] C --> AG["Order (Aggregate)"] AG --> E["OrderPlaced (Domain Event)"] E --> P["Policy: whenever PaymentCaptured แล้วสั่ง AcceptOrder"] classDef actor fill:#fde047,stroke:#a16207,color:#1a1a1f; classDef cmd fill:#3b82f6,stroke:#1e40af,color:#f8fafc; classDef agg fill:#fef3c7,stroke:#b45309,color:#1a1a1f; classDef evt fill:#e8743b,stroke:#b45309,color:#1a1a1f; classDef pol fill:#c4b5fd,stroke:#6d28d9,color:#1a1a1f; class A actor; class C cmd; class AG agg; class E evt; class P pol;
เขียนเป็น code ตรงๆ ก็ได้หน้าตาแบบนี้ หัวใจอยู่ตรงที่ กฎอยู่ข้างใน class และ command เป็นทางเข้าเดียวที่จะเปลี่ยนสถานะได้:
class Order { private items: OrderLine[] = []; private status: OrderStatus = "Draft";
// Command = method: ตรวจกฎก่อน แล้วค่อยเปลี่ยนสถานะ placeOrder(): OrderPlaced { if (this.items.length === 0) { throw new Error("สั่งอาหารโดยตะกร้าว่างไม่ได้"); } this.status = "Placed"; // Event = สิ่งที่ publish ออกไปหลังกฎผ่าน return new OrderPlaced(this.id, this.items); }}กฎ “ตะกร้าว่างสั่งไม่ได้” คือ invariant — สิ่งที่ต้องจริงเสมอไม่ว่าใครจะทำอะไรกับ Order และเพราะมันอยู่ใน placeOrder() จึงไม่มีทางที่ใครจะข้ามมันไปได้ class นี้ปกป้องความถูกต้องของตัวเอง นี่คือความหมายของ “หน่วยความสอดคล้อง (consistency boundary)” ที่คอร์ส DDD พูดถึง แต่คราวนี้เราได้มันมาจาก Post-it เหลืองใหญ่บนผนัง
กับดักคลาสสิกคือเปลี่ยน Aggregate เป็น class ที่มีแต่ getter/setter ไม่มีกฎอยู่ข้างใน แล้วผลัก logic ทั้งหมดไปไว้ที่ service ข้างนอก แบบนี้:
class Order { items: OrderLine[] = []; status: string = "";}class OrderService { place(order: Order) { order.status = "Placed"; // ใครก็เซ็ตได้ ไม่มีใครกันตะกร้าว่าง }}โครงแบบนี้เรียกว่า anemic domain model — มันดูเหมือน object แต่ข้างในกลวง กฎ (invariant) รั่วออกไปกองที่ service ทำให้ไม่มีใครเป็นเจ้าของความถูกต้องของ Order อีกต่อไป ผลของ Event Storming ที่ชัดเจน (aggregate ตัวไหนปล่อย event อะไร ใต้กฎอะไร) คือยาแก้กับดักนี้โดยตรง เพราะมันบอกเราตั้งแต่ต้นว่า กฎอะไรต้องอยู่ใน class ไหน
Policy สู่ Process Manager และ Integration Event
หัวข้อที่มีชื่อว่า “Policy สู่ Process Manager และ Integration Event”Post-it สีม่วง — Policy แบบ “whenever event แล้วสั่ง command” — แปลงเป็น code ได้สองรูปแบบ ขึ้นกับว่ามันข้ามขอบเขตหรือไม่
ถ้า event กับ command ที่มันเชื่อม อยู่ใน context เดียวกัน policy ก็เป็นแค่ handler เล็กๆ ที่ subscribe event แล้วยิง command ต่อ แต่ถ้า policy ต้อง ประสานงานข้าม aggregate หรือข้าม context และมีสถานะที่ต้องจำระหว่างขั้นตอน (เช่น “รอ PaymentCaptured ภายใน 5 นาที ถ้าไม่มาให้ยกเลิกออเดอร์”) มันจะโตขึ้นเป็น process manager หรือ saga — object ที่คอยติดตามว่ากระบวนการยาวๆ เดินไปถึงไหนแล้ว และตัดสินใจ step ถัดไป
จุดสำคัญคือ event ที่ policy ฝั่งหนึ่งฟัง อาจถูกปล่อยจากอีก context หนึ่ง — เมื่อ event เดินทางข้ามเส้น Bounded Context เราเรียกมันว่า integration event (ต่างจาก domain event ภายในที่ไม่เคยออกนอกขอบเขต) ในฟู้ดเดลิเวอรี policy “whenever PaymentCaptured แล้วแจ้งร้านให้กดรับ” คือ integration event ที่ Payment ปล่อยให้ Dispatch ฟัง ส่วน “whenever OrderAcceptedByRestaurant แล้วหาไรเดอร์” เป็น policy ภายใน Dispatch เอง การแยกสองชนิดนี้ให้ชัดตั้งแต่บนผนัง ช่วยให้ตอนออกแบบ integration รู้ว่า event ไหนคือ “สัญญาสาธารณะ” ที่ห้ามเปลี่ยนพล่อยๆ
Event Storming คือสะพานกลับสู่ DDD
หัวข้อที่มีชื่อว่า “Event Storming คือสะพานกลับสู่ DDD”ถอยออกมามองภาพรวม จะเห็นว่าทุกอย่างคือการเดินกลับเข้าสู่คอร์ส DDD แบบมีของติดมือ:
- ผลฝั่ง strategic design — pivotal event ให้ Bounded Context, รอยต่อระหว่างกลุ่ม event ให้ context map และ ACL, คำบน Post-it ให้ Ubiquitous Language ที่ทุกคนเถียงกันจนตกผลึกแล้ว
- ผลฝั่ง tactical design — aggregate ให้ class ที่ถือ invariant, command ให้ method, domain event ให้สิ่งที่ publish, policy ให้ process manager/saga
Event Storming ไม่ได้มาแทน DDD และไม่ได้แข่งกับ DDD — มันคือ สะพานให้คนเดินข้าม จากห้องเวิร์กช็อปที่เต็มไปด้วยผู้เชี่ยวชาญธุรกิจ ไปสู่ model และ code ที่นักพัฒนาลงมือทำ ปัญหาใหญ่สุดของ DDD ที่ทำจริงแล้วพังบ่อยๆ คือ “ขุด domain ไม่ออก” หรือ “domain expert กับ dev พูดกันคนละภาษา” Event Storming แก้ตรงนั้นก่อนที่จะไปแตะ tactical pattern ด้วยซ้ำ
ลำดับที่ใช้ได้จริงคือ Big Picture EventStormingBig Picture EventStormingเวิร์กช็อป EventStorming รอบแรกและกว้างที่สุด เชิญคนจากทุกฝ่ายมาระดม domain event ของทั้งสายธุรกิจลงบนไทม์ไลน์เดียว เป้าหมายคือเห็นภาพรวม ค้นพบรอยต่อและจุดที่ยังไม่รู้ ไม่เน้นรายละเอียดProcess เพื่อเห็นทั้งสายธุรกิจ → Process ModelingProcess Modelingระดับกลางของ EventStorming ที่ต่อจาก Big Picture — เพิ่ม command, actor, policy และ read model เข้ามาเพื่อให้เห็น “กลไก” ที่ทำให้แต่ละ event เกิดขึ้น ก่อนจะลงลึกระดับ designProcess เพื่อเติม command/policy/read model → Design-Level EventStormingDesign-Level EventStormingEventStorming รอบเจาะลึกเข้าไปใน bounded context เดียว เพื่อออกแบบระดับที่พร้อมเขียน code โดยเรียงลำดับ command → aggregate → event และ policy อย่างละเอียดProcess เพื่อซูมเข้า1 context ให้พร้อมเขียน code แล้วค่อยหยิบ tactical pattern ของ DDD มาลงมือ ทุกก้าวมีของจากก้าวก่อนหน้าส่งต่อ ไม่มีช่วงไหนที่ต้องเริ่มจากกระดาษเปล่า
ไปต่อที่ไหน
หัวข้อที่มีชื่อว่า “ไปต่อที่ไหน”จบคอร์ส Event Storming แล้ว ถ้าอยากลงลึกฝั่ง design และ code ต่อ แนะนำเดินตามนี้:
- คอร์ส Domain-Driven Design ของเว็บ — เจาะ strategic (bounded context, context map, ubiquitous language) และ tactical (aggregate, entity, value object) แบบเต็มๆ ต่อยอดจากผลเวิร์กช็อปที่เพิ่งได้มา
- คอร์ส DDD Distilled — ฉบับกระชับของ Vaughn Vernon เหมาะถ้าอยากได้ภาพรวม DDD เร็วๆ ก่อนลงลึก
- คอร์ส Patterns, Principles, and Practices of DDD — ลึกที่สุดในเรื่อง tactical pattern และการวางสถาปัตยกรรมรอบๆ model
ผนัง Post-it ที่คุณสร้างในคอร์สนี้จะไม่สูญเปล่า — มันคือแผนที่ที่พาคุณเดินเข้าสู่โลก DDD โดยไม่หลง
เจาะลึกแนวคิดต่อได้ที่คลังอ้างอิง DevIQ:
- Bounded Context — นิยามขอบเขตที่ pivotal event ช่วยให้เราค้นพบจากไทม์ไลน์
- Context Map — แผนที่ความสัมพันธ์และทิศทางการพึ่งพาระหว่าง context ที่เราวาด
- Aggregate — จาก Post-it เหลืองใหญ่สู่ class ที่ถือ invariant และรับ command เป็น method
- Anemic Domain Model — กับดักที่ต้องเลี่ยงตอนแปลง aggregate เป็น code
- Tactical Design — building block ที่ใช้ต่อจากผลเวิร์กช็อปเมื่อลงมือเขียนจริง
เช็กความเข้าใจ — บทที่ 6
ข้อ 1 / 3pivotal event สัมพันธ์กับ bounded context อย่างไร?