Process & Design-Level EventStorming
สามบทที่ผ่านมาเราแปะ Post-it สีส้มเต็มผนังใน Big Picture EventStormingBig Picture EventStormingเวิร์กช็อป EventStorming รอบแรกและกว้างที่สุด เชิญคนจากทุกฝ่ายมาระดม domain event ของทั้งสายธุรกิจลงบนไทม์ไลน์เดียว เป้าหมายคือเห็นภาพรวม ค้นพบรอยต่อและจุดที่ยังไม่รู้ ไม่เน้นรายละเอียดProcess จนเห็นเรื่องราวของธุรกิจฟู้ดเดลิเวอรีทั้งเส้น — ตั้งแต่ ItemAddedToCart ยาวไปจนถึง OrderRated เราขีดเส้นแบ่งช่วงด้วย Pivotal EventPivotal Eventdomain event สำคัญที่แบ่งไทม์ไลน์ออกเป็นช่วง ๆ (เช่น OrderPlaced, FoodDelivered) มักขีดเส้นแนวตั้งกำกับ ใช้เป็นเบาะแสของขอบเขต bounded context ในภายหลังProcess อย่าง OrderPlaced, PaymentCaptured, FoodDelivered และเก็บคำถามค้างคาไว้ใน Post-it สีแดง (HotspotHotspotจุดที่เป็นปัญหา ข้อขัดแย้ง หรือคำถามที่ยังตอบไม่ได้ระหว่างเวิร์กช็อป แทนด้วย Post-it สีแดงวางเฉียง ๆ เก็บไว้กลับมาคุยทีหลังโดยไม่ขัดจังหวะการไหลของกลุ่มProcess) เรียบร้อย
ภาพกว้างแบบนั้น “เห็นป่าทั้งผืน” แต่ยังเข้าไปสร้างบ้านไม่ได้ บทนี้จะซูมกล้องจากป่าลงมาที่ต้นไม้ต้นเดียว — เลือก Bounded ContextBounded Contextขอบเขตที่ model และ ubiquitous language หนึ่งใช้ได้และมีความหมายแน่นอน ใน EventStorming มักค้นพบขอบเขตนี้จาก pivotal event และกลุ่ม event ที่เกาะกลุ่มกันStrategic Design หนึ่ง (จะใช้ Ordering เป็นหลัก) แล้วขยายจนละเอียดพอที่โปรแกรมเมอร์หยิบไปเขียน code ได้ทันที เครื่องมือที่พาเราไปถึงตรงนั้นมีสองระดับที่ต่อกัน คือ Process ModelingProcess Modelingระดับกลางของ EventStorming ที่ต่อจาก Big Picture — เพิ่ม command, actor, policy และ read model เข้ามาเพื่อให้เห็น “กลไก” ที่ทำให้แต่ละ event เกิดขึ้น ก่อนจะลงลึกระดับ designProcess และ Design-Level EventStormingDesign-Level EventStormingEventStorming รอบเจาะลึกเข้าไปใน bounded context เดียว เพื่อออกแบบระดับที่พร้อมเขียน code โดยเรียงลำดับ command → aggregate → event และ policy อย่างละเอียดProcess
จาก Big Picture สู่ห้องเครื่อง: Process Modeling เป็นสะพาน
หัวข้อที่มีชื่อว่า “จาก Big Picture สู่ห้องเครื่อง: Process Modeling เป็นสะพาน”Big Picture ให้เราแค่ “event เกิดอะไรบ้าง เรียงยังไง” แต่ไม่ได้บอกว่า event แต่ละอันเกิดขึ้นได้อย่างไร ใครสั่ง? สั่งเพราะเห็นข้อมูลอะไร? มีกฎอะไรมากระตุ้นให้เกิด event ถัดไปโดยอัตโนมัติ?
Process Modeling คือระดับกลางที่มาตอบคำถามพวกนี้ วิธีทำง่ายมาก — เดินไปทีละ event แล้ว “เติมของรอบๆ มัน” เข้าไป จนเห็นกลไก:
- ข้างหน้า event มักมี CommandCommandความตั้งใจหรือการตัดสินใจที่ “สั่งให้เกิด” domain event เขียนเป็นกริยาคำสั่ง (เช่น PlaceOrder) แทนด้วย Post-it สีฟ้า มักมี actor เป็นผู้ลงมือTactical Design (สีฟ้า) ที่เป็น “ความตั้งใจสั่งให้เกิด” เช่นก่อนจะได้
OrderPlacedต้องมีคนสั่งPlaceOrder - ข้างหลัง command มักมี ActorActorคน/บทบาทที่ออก command (เช่น ลูกค้า ร้านอาหาร ไรเดอร์) แทนด้วย Post-it สีเหลืองแผ่นเล็กแปะมุมของ command ช่วยตอบว่า “ใครเป็นคนทำ”Process (สีเหลืองแผ่นเล็ก) คือคนที่ลงมือ เช่น ลูกค้า ร้านอาหาร ไรเดอร์
- ก่อน actor จะตัดสินใจ เขามักมองอะไรบางอย่างอยู่ — นั่นคือ Read ModelRead Modelข้อมูล/หน้าจอที่ผู้ใช้ “อ่าน” เพื่อประกอบการตัดสินใจก่อนออก command (เช่น ตะกร้า หน้าติดตามออเดอร์) แทนด้วย Post-it สีเขียว เชื่อมโยงกับฝั่ง Query ของ CQRSTactical Design (สีเขียว) เช่นหน้าตะกร้า (Cart) ที่ให้เห็นยอดรวมก่อนกดสั่ง
- บาง event ไม่ได้เกิดจากคน แต่เกิดจากกฎอัตโนมัติ — PolicyPolicyกฎเชิงปฏิกิริยาแบบ “whenever X → do Y” ที่คอยฟัง event หนึ่งแล้วสั่ง command ต่อโดยอัตโนมัติ (เช่น เมื่อ PaymentCaptured → แจ้งร้านให้กดรับ) แทนด้วย Post-it สีม่วงอ่อนProcess (สีม่วง) แบบ “whenever event นี้ → สั่ง command โน้น”
- และบาง command วิ่งออกไปหา External SystemExternal Systemระบบภายนอกที่เราไม่ได้ควบคุมแต่ต้องคุยด้วย (เช่น Payment Gateway, บริการแจ้งเตือน, แผนที่) แทนด้วย Post-it สีชมพู ช่วยชี้จุดเชื่อมต่อและความเสี่ยงArchitecture (สีชมพู) ที่เราไม่ได้คุม เช่น Payment Gateway
พอเติมครบ ไทม์ไลน์ที่เคยมีแต่ Post-it สีส้มก็กลายเป็น “เรื่องราวที่อธิบายกลไกตัวเอง” ได้ นี่แหละสะพานที่พาเราจากภาพรวมไปสู่ระดับ design
ถ้ากระโดดจาก Big Picture ไป design-level เลย ทีมมักเถียงกันเรื่อง class กับ table เร็วเกินไปทั้งที่ยังไม่เห็นภาพกลไกร่วมกัน Process Modeling บังคับให้เราตอบ “ใครสั่ง เพราะเห็นอะไร กฎอะไรทำงานต่อ” ก่อน แล้วค่อยลงรายละเอียดโครงสร้าง — ลำดับนี้ช่วยกันเสียเวลาไปกับ design ที่ยังไม่เข้าใจ domain จริง
ไวยากรณ์ของ Design-Level: command → aggregate → event
หัวข้อที่มีชื่อว่า “ไวยากรณ์ของ Design-Level: command → aggregate → event”พอเข้า Design-Level EventStorming จะจัดของทั้งหมดให้เข้ารูปประโยคเดียวที่ซ้ำไปทั้งผนัง เรียกว่า “ไวยากรณ์” ของ EventStorming:
actor เห็น read model แล้วออก command → command วิ่งเข้า aggregate ตัวหนึ่ง → aggregate ตัดสินใจแล้วปล่อย event ออกมา
ลองดู slice เดียวจากฟู้ดเดลิเวอรี — ตอนลูกค้ากดสั่งอาหาร:
flowchart LR RM["ตะกร้า Cart<br/>read model"] --> A["ลูกค้า<br/>actor"] A --> C["PlaceOrder<br/>command"] C --> AGG["Order<br/>aggregate"] AGG --> E["OrderPlaced<br/>event"] classDef rm fill:#86efac,stroke:#15803d,color:#1a1a1f; classDef act fill:#fde047,stroke:#a16207,color:#1a1a1f; classDef cmd fill:#3b82f6,stroke:#1e40af,color:#f8fafc; classDef agg fill:#fef3c7,stroke:#a16207,color:#1a1a1f; classDef evt fill:#e8743b,stroke:#b45309,color:#1a1a1f; class RM rm; class A act; class C cmd; class AGG agg; class E evt;
อ่านจากซ้ายไปขวา: ลูกค้าเห็นยอดในตะกร้า จึงออกคำสั่ง PlaceOrder คำสั่งนี้ไม่ได้ลอยไปไหน มันวิ่งเข้าหา Order ซึ่งเป็น aggregate แล้ว Order เป็นคนตัดสินใจว่า “รับได้ไหม” ถ้าผ่านกฎก็ปล่อย OrderPlaced ออกมา — ประโยคเดียวนี้ซ้ำเป็นสิบๆ ครั้งตลอด context จนเห็น design ทั้งก้อน
จุดสำคัญที่มือใหม่มักพลาด คือ event ไม่ได้ต่อกับ event ตรงๆ เหมือน Big Picture อีกต่อไป ระหว่าง event สองอันจะมีกลไก (command + aggregate หรือ policy) คั่นเสมอ ถ้าเจอ event ต่อ event โดยไม่มีอะไรคั่น แปลว่าเรายังขุดกลไกไม่ครบ
Aggregate คือหน่วยความสอดคล้อง (Consistency Boundary)
หัวข้อที่มีชื่อว่า “Aggregate คือหน่วยความสอดคล้อง (Consistency Boundary)”หัวใจของ design-level คือคำว่า AggregateAggregateกลุ่มของ object ที่ถือกฎความสอดคล้อง (invariant) ไว้ด้วยกัน ใน EventStorming คือ “สิ่งที่รับ command แล้วปล่อย event ออกมา” (เช่น Order, Payment) แทนด้วย Post-it สีเหลืองแผ่นใหญ่Tactical Design ในภาษา EventStorming มันคือ “ก้อนที่รับ command เข้าไป แล้วตัดสินใจว่าจะปล่อย event อะไรออกมา” — และที่สำคัญกว่านั้น มันคือ ขอบเขตความสอดคล้อง (consistency boundary)
หมายความว่าอะไร? หมายความว่ากฎธุรกิจที่ “ต้องเป๊ะทันที” จะถูกปกป้องอยู่ภายใน aggregate ตัวเดียว ยกตัวอย่าง Order ต้องรับประกันว่า “ยอดรวมของออเดอร์ = ผลบวกของทุกรายการอาหาร” กฎนี้พลาดไม่ได้แม้แต่วินาทีเดียว มันเลยต้องอยู่ในกำแพงของ Order
กติกาเหล็กของ design-level มีข้อเดียวที่ต้องจำ:
1 command เข้า 1 aggregate แล้วปล่อย event ออกมา ในหนึ่งการทำงาน (1 transaction) เราแตะ aggregate แค่ตัวเดียว
ในฟู้ดเดลิเวอรีเรามี aggregate หลักสามตัว แต่ละตัวคุมกฎของตัวเอง:
Order— รับPlaceOrderปล่อยOrderPlacedปกป้องกฎเรื่องรายการอาหารกับยอดรวมPayment— รับCapturePaymentปล่อยPaymentCapturedปกป้องกฎว่าตัดเงินได้ครั้งเดียวต่อออเดอร์Delivery— รับAssignRider/ConfirmDeliveryปล่อยRiderAssigned/FoodDelivered
สังเกตว่า Order ไม่ได้ไปยุ่งกับการตัดเงิน และ Payment ก็ไม่รู้เรื่องรายการอาหาร ต่างคนต่างเฝ้ากฎของตัวเอง — นี่คือเหตุผลที่ aggregate ถูกเรียกว่า “หน่วยความสอดคล้อง” เพราะมันคือหน่วยที่เล็กที่สุดที่การันตีความถูกต้องได้ในหนึ่งครั้ง
มือใหม่มักออกแบบ Order ก้อนยักษ์ที่ถือทั้งรายการอาหาร การจ่ายเงิน ที่อยู่จัดส่ง และสถานะไรเดอร์ไว้ในตัวเดียว ผลคือทุกๆ การอัปเดตเล็กน้อยต้องล็อกทั้งก้อน แก้ชนกันตลอด กฎคือ ถ้าสองสิ่งไม่จำเป็นต้องถูกต้องพร้อมกัน ณ วินาทีเดียวกัน ก็แยกเป็นคนละ aggregate แล้วค่อยเชื่อมกันด้วย policy (หัวข้อถัดไป)
Policy: กาวที่เชื่อม Aggregate ข้ามขอบเขต
หัวข้อที่มีชื่อว่า “Policy: กาวที่เชื่อม Aggregate ข้ามขอบเขต”ถ้าแต่ละ aggregate แตะได้ทีละตัวต่อ transaction แล้ว event ที่ Payment ปล่อยออกมาจะไปสั่งให้ร้านกดรับได้ยังไง? คำตอบคือ Policy — Post-it สีม่วงที่อ่านว่า “whenever <event> → <command>”
Policy คือกฎเชิงปฏิกิริยาที่นั่งฟัง event หนึ่งอยู่เงียบๆ พอ event นั้นเกิดปุ๊บ มันก็ยิง command ตัวใหม่ออกไปหา aggregate อีกตัวโดยอัตโนมัติ นี่คือกลไกที่ทำให้กระบวนการ “ไหลต่อ” ข้ามขอบเขตของ aggregate ได้ โดยไม่ต้องแตะ2 aggregate พร้อมกัน
ในฟู้ดเดลิเวอรีมี policy หลายตัวที่คอยส่งไม้ต่อให้กระบวนการเดินเอง เช่น:
- whenever
OrderPlaced→ ตัดเงิน (นำไปสู่CapturePayment) - whenever
PaymentCaptured→ แจ้งร้านให้กดรับ (นำไปสู่AcceptOrder) - whenever
OrderAcceptedByRestaurant→ หาไรเดอร์ (นำไปสู่AssignRider)
ลองวางสองตัวแรกเป็นโซ่ต่อกันตั้งแต่ลูกค้ากดสั่ง จนกลไกวิ่งข้าม3 aggregate ด้วยตัวเอง:
flowchart LR A["ลูกค้า"] --> C1["PlaceOrder"] C1 --> O["Order aggregate"] O --> E1["OrderPlaced"] E1 --> P1["whenever OrderPlaced<br/>ตัดเงิน"] P1 --> C2["CapturePayment"] C2 --> PAY["Payment aggregate"] PAY --> E2["PaymentCaptured"] E2 --> P2["whenever PaymentCaptured<br/>แจ้งร้านให้กดรับ"] P2 --> C3["AcceptOrder"] C3 --> O2["Order aggregate"] O2 --> E3["OrderAcceptedByRestaurant"] classDef act fill:#fde047,stroke:#a16207,color:#1a1a1f; classDef cmd fill:#3b82f6,stroke:#1e40af,color:#f8fafc; classDef agg fill:#fef3c7,stroke:#a16207,color:#1a1a1f; classDef evt fill:#e8743b,stroke:#b45309,color:#1a1a1f; classDef pol fill:#c4b5fd,stroke:#6d28d9,color:#1a1a1f; class A act; class C1 cmd; class C2 cmd; class C3 cmd; class O agg; class PAY agg; class O2 agg; class E1 evt; class E2 evt; class E3 evt; class P1 pol; class P2 pol;
อ่านโซ่นี้แล้วจะเห็น “จังหวะหายใจ” ของระบบ: คนเริ่มต้นแค่ครั้งเดียว (PlaceOrder) จากนั้น policy สีม่วงทำหน้าที่เป็นกาว ส่งต่อจาก event ของ aggregate หนึ่งไปเป็น command ของอีก aggregate อย่างต่อเนื่องจนกระบวนการเดินเองได้ ตรงนี้แหละที่ design-level เผยกลไกซึ่ง Big Picture มองไม่เห็น
ลองอ่าน slice Ordering ออกเสียงเป็นภาษาคน: “ลูกค้าเห็นตะกร้า จึง PlaceOrder; Order รับแล้วปล่อย OrderPlaced; whenever OrderPlaced ระบบ CapturePayment; Payment ปล่อย PaymentCaptured; whenever PaymentCaptured แจ้งร้านให้ AcceptOrder; Order ปล่อย OrderAcceptedByRestaurant” — ถ้าอ่านทั้งผนังแล้วลื่นเป็นเรื่องเล่าแบบนี้ แปลว่า design ครบพอจะลง code แล้ว ถ้าสะดุดตรงไหน แปลว่าตรงนั้นยังมีกลไกที่ทีมยังไม่เข้าใจตรงกัน
Read Model กับ CQRS: แยกฝั่งเขียนออกจากฝั่งอ่าน
หัวข้อที่มีชื่อว่า “Read Model กับ CQRS: แยกฝั่งเขียนออกจากฝั่งอ่าน”ทีนี้ย้อนไปที่ Post-it สีเขียว — Read Model มันไม่ใช่ของประดับ แต่เป็นครึ่งหนึ่งของภาพที่คนมักลืม
สังเกตว่าในไวยากรณ์ของเรา event เป็นผลลัพธ์ของการ “เขียน” (command เปลี่ยนสถานะ) แต่ actor จะออก command ได้ก็ต่อเมื่อ “อ่าน” อะไรบางอย่างก่อน เช่นลูกค้าต้องเห็นตะกร้าก่อน ถึงจะกดสั่ง; ต้องเห็นหน้าติดตามออเดอร์ (Order tracking) ก่อน ถึงจะรู้ว่าต้องรออีกนานไหม
พอแยกสองฝั่งนี้ออกจากกันชัดๆ เราก็ได้แนวคิดที่ชื่อ CQRS (Command Query Responsibility Segregation) — แยก “ความรับผิดชอบของการสั่ง (Command)” ออกจาก “ความรับผิดชอบของการถาม (Query)” คนละ model คนละเส้นทาง:
flowchart LR
subgraph WRITE["ฝั่งเขียน Command หรือ Write"]
A["ลูกค้า"] --> CMD["PlaceOrder"]
CMD --> AGG["Order aggregate"]
AGG --> EV["OrderPlaced"]
end
subgraph READ["ฝั่งอ่าน Query หรือ Read"]
A2["ลูกค้า"] --> RM["หน้าติดตามออเดอร์<br/>read model"]
end
EV -.->|"อัปเดต projection"| RM
classDef act fill:#fde047,stroke:#a16207,color:#1a1a1f;
classDef cmd fill:#3b82f6,stroke:#1e40af,color:#f8fafc;
classDef agg fill:#fef3c7,stroke:#a16207,color:#1a1a1f;
classDef evt fill:#e8743b,stroke:#b45309,color:#1a1a1f;
classDef rm fill:#86efac,stroke:#15803d,color:#1a1a1f;
class A act;
class A2 act;
class CMD cmd;
class AGG agg;
class EV evt;
class RM rm;
ฝั่งเขียน (ซ้าย) มีหน้าที่เดียว — รับ command ปกป้องกฎ ปล่อย event มันไม่ต้องแคร์ว่าหน้าจอจะโชว์สวยแค่ไหน ส่วนฝั่งอ่าน (ขวา) มีหน้าที่เดียวเช่นกัน — เอา event ที่เกิดขึ้นมา “ปั้น” เป็นหน้าจอที่ตอบคำถามผู้ใช้ได้เร็ว (เรียกว่า projection) เส้นประจาก OrderPlaced ไป read model คือการบอกว่า “ทุกครั้งที่ฝั่งเขียนปล่อย event ฝั่งอ่านจะอัปเดตหน้าจอตาม”
ข้อดีที่จับต้องได้: ฝั่งอ่านปรับแต่งให้เร็วได้อิสระ (cache ทำ index ตามใจ) โดยไม่กระทบกฎธุรกิจฝั่งเขียนเลย และเวลาทำ EventStorming จะเห็นชัดว่า read model แต่ละอันถูกป้อนด้วย event ตัวไหนบ้าง — ทำให้ตอบได้ทันทีว่า “ถ้าอยากได้หน้าจอนี้ ต้องมี event อะไรเกิดก่อน”
พอสนุกกับ policy หลายทีมเริ่มโยง event ทุกอันเข้าหากันจนผนังกลายเป็นชามสปาเกตตี วิธีกันคือถามทุกเส้นสีม่วงว่า “กฎนี้เป็นของ domain จริงไหม หรือแค่รายละเอียดเทคนิค” และ “ใครเป็นเจ้าของกฎนี้” ถ้าตอบไม่ได้ ให้แปะเป็น Hotspot สีแดงไว้ก่อน อย่าเพิ่งลากเส้น
Design-Level ต่างจาก Big Picture ตรงไหน
หัวข้อที่มีชื่อว่า “Design-Level ต่างจาก Big Picture ตรงไหน”ปิดท้ายด้วยการเทียบสองระดับให้เห็นชัด เพื่อไม่ให้สับสนว่าตอนไหนควรอยู่ระดับไหน:
- ขอบเขต — Big Picture กวาดทั้งสายธุรกิจหลาย context; design-level ซูมเข้า context เดียว (เช่น Ordering) แล้วเจาะให้ลึก
- คนในห้อง — Big Picture เชิญคนทุกฝ่ายมาระดม; design-level เหลือทีมที่จะลงมือสร้างจริง (dev + domain expert ที่เกี่ยวข้อง)
- หน่วยบนผนัง — Big Picture มีแทบแต่ event สีส้ม; design-level ครบทุกสี — command, aggregate, policy, read model, external
- ความละเอียด — Big Picture ตอบ “เกิดอะไรบ้าง”; design-level ตอบ “เกิดได้ยังไง ใครสั่ง กฎไหนทำงาน” จนพร้อมแปลงเป็น code
- ผลลัพธ์ — Big Picture ได้ภาพรวมและ pivotal event ที่บอกใบ้ขอบเขต; design-level ได้พิมพ์เขียวของ aggregate, command, event ที่หยิบไปออกแบบ class/ตารางได้เลย
ทั้งสองไม่ใช่คู่แข่ง แต่เป็นกล้องคนละกำลังขยาย เราเริ่มจากเลนส์กว้างเพื่อไม่ให้หลงป่า แล้วค่อยเปลี่ยนเป็นเลนส์มาโครเมื่อรู้แล้วว่าจะเจาะต้นไหน
- Process Modeling เป็นสะพาน — เดินทีละ event แล้วเติม command/actor/read model/policy/external รอบๆ จนเห็นกลไกที่ทำให้ event เกิด
- ไวยากรณ์ design-level — actor เห็น read model → ออก Command → เข้า 1 Aggregate → ปล่อย Domain EventDomain Eventสิ่งที่ “เกิดขึ้นแล้ว” และมีความหมายต่อผู้เชี่ยวชาญธุรกิจ เขียนเป็นกริยาอดีต (เช่น OrderPlaced, PaymentCaptured) แทนด้วย Post-it สีส้ม เป็นหน่วยตั้งต้นของ EventStorming ทุกแบบTactical Design; event ไม่ต่อ event ตรงๆ อีกต่อไป
- Aggregate = ขอบเขตความสอดคล้อง — 1 command เข้า 1 aggregate ต่อ1 transaction; สิ่งที่ต้องถูกต้องพร้อมกันเท่านั้นจึงอยู่ก้อนเดียวกัน
- Policy เชื่อมข้ามขอบเขต — “whenever event → command” ทำให้กระบวนการไหลข้าม aggregate โดยไม่ต้องแตะสองตัวพร้อมกัน
- Read Model + CQRS — ฝั่งเขียน (command/aggregate/event) แยกจากฝั่งอ่าน (read model/projection) คนละ model คนละเส้นทาง
เจาะลึกแนวคิดต่อได้ที่คลังอ้างอิง DevIQ:
- Aggregate — ขอบเขตความสอดคล้องที่ design-level ยึดเป็นหน่วย 1 command เข้า 1 aggregate ตรงกับหัวใจของบทนี้
- CQRS — แนวคิดแยกฝั่งเขียน (command) ออกจากฝั่งอ่าน (read model) แบบเต็มๆ ต่อยอดจากหัวข้อ Read Model
- Event Aggregator — กลไก pub/sub เบื้องหลังที่ทำให้ policy คอยฟัง event แล้วยิง command ต่อได้จริงใน code
เช็กความเข้าใจ — บทที่ 4
ข้อ 1 / 3ใน Design-Level EventStorming aggregate เป็น “ขอบเขต” ของอะไรเป็นอันดับแรก?