ข้าม​ไป​ยัง​เนื้อหา

รู้จัก​การ​เชื่อม Bounded Context เข้า​ด้วย​กัน

หลัง​จาก​เรา​ระบุ Bounded ContextBounded Contextขอบเขต​ที่​ชัดเจน (มัก​เป็น​ระบบ​ย่อย​หรือ​ทีม​หนึ่ง) ที่ model หนึ่ง​ใช้ได้​และ​มี​ความหมาย​แน่นอน ภายใน​ขอบเขต​นี้ Ubiquitous Language สอดคล้อง​กัน 100%Strategic Design ใน​ระบบ​ได้​แล้ว (จาก​เรื่อง context mappingContext Mapแผนภาพ​ที่​แสดง Bounded Context ทั้งหมด​และ​ความ​สัมพันธ์​ระหว่าง​กัน ช่วย​ให้​เห็น​ภาพ​รวม​และ​จุด​เชื่อม​ต่อ/แปล​ภาษา​ใน​ระบบ​ใหญ่Strategic Design) คำถาม​ถัด​มา​คือ เรา​จะ “เชื่อม” มัน​เข้า​ด้วย​กัน​อย่างไร เพื่อ​ให้​ทำ business use case เต็ม​รูปแบบ​ได้​สำเร็จ — เพราะ​ใน​ความ​เป็น​จริง ขั้น “รับคำ​สั่ง​ซื้อ”, “เก็บ​เงิน”, และ “จัด​ส่ง” มัก​อยู่​คนละ context กัน อาจ​เป็น​ซอฟต์แวร์​คนละ​ชิ้น รัน​คนละ​เครื่อง หรือ​คนละ cloud instance

บท​นี้​เป็น บท​ปู​พื้น ของ​การ​เชื่อม context ใน​ระบบ​กระจาย เรา​จะ​ยัง​ไม่​ลง code เต็มๆ (อยู่​ใน​สอง​บท​ถัด​ไป) แต่​จะ​ทำความ​เข้าใจ “แนวคิด​พื้นฐาน​ของ distributed computing” ที่​ทำให้​เรา​รักษา​ความ​ชัดเจน​ของ domain model ไว้​ได้ ขณะ​เดียวกัน​ก็​รับมือ​กับ nonfunctional requirements อย่าง scalability และ reliability ที่​ติด​มา​กับ​ระบบ​กระจาย​โดย​ธรรมชาติ

หัวใจ​ของ​บท​นี้

การ​เชื่อม Bounded Context มี​สอง​ด้าน​ที่​ต้อง​จัดการ​เสมอ: ด้าน​เทคนิค (scalability, reliability) และ ด้าน​สังคม (หลาย​ทีม​ต้อง​ทำงาน​ขนาน​กัน​โดย​ไม่​ขวาง​กัน) ทาง​เลือก​ใน​การ​สื่อสาร​ที่​คุณ​เลือก — database, RPC, หรือ messaging — คือ​สิ่ง​ที่​ตัดสิน​ว่า​ระบบ​จะ​รองรับ​ผู้​ใช้​นับ​สิบ​ล้าน​คน​ได้ หรือ​จะ​ล่ม​ทั้ง​ระบบ​ใน​วัน​ที่ traffic พุ่ง​สูงสุด


เมื่อ​ระบบ​โต​ขึ้น dependency จะ​กลาย​เป็น​ภาระ​มากกว่า​ประโยชน์ เรา​ควร​เลี่ยง coupling ทุก​รูปแบบ​เว้น​แต่​มี​เหตุผล​ที่​ดีจริงๆ เพราะ coupling มี​สอง​หน้า:

  • coupling ที่​ระดับ code — ทีม​หนึ่ง​แก้ code แล้ว​ทำ code อีก​ทีม​พัง หรือ​กลาย​เป็น​คอ​ขวด​ที่​ทำให้การ​ส่ง feature ช้า​ลง
  • coupling ตอน runtime — ระบบ​หนึ่ง​ทำงาน​ไม่​ได้​ถ้า​อีก​ระบบ​หนึ่ง​ล่ม

ถ้า​เรา​ออกแบบ context ให้ loosely coupled แต่ละ context จะ​พัฒนา​แยก​กัน​ได้ codebase ของ​มัน​วิวัฒน์​ได้​โดย​ไม่​ต้อง​กังวล​ว่า​จะ​ทำ​กระบวนการ​ธุรกิจ (business process) ของ context อื่น​พัง และ​นัก​พัฒนา​ก็​ไม่​ต้อง​รอ​ทีม​อื่น​อนุมัติ​การ​เปลี่ยนแปลง ผล​คือ​คอ​ขวด​น้อย​ลง และ​ส่ง​มอบ​คุณค่า​ทาง​ธุรกิจ​ได้​เร็ว​ขึ้น แนวคิด​นี้​สอดคล้อง​กับ Single Responsibility Principle (SRP) โดยตรง — แยก​ความ​สามารถ​ทาง​ธุรกิจ​ที่​ต่าง​กัน (sales, shipping) ออก​เป็น​คนละ context

การ​วาง context ทั้งหมด​ไว้​ใน solution เดียว​มี​ข้อดี​คือ​ช่วย​ให้​นัก​พัฒนา​เห็น “ภาพ​ใหญ่” ว่า​มี​โลก​นอก context ของ​ตัวเอง แต่​ก็​เพิ่ม​ความ​เสี่ยง​ที่​จะ​เกิด dependency ระหว่าง​กัน ลอง​นึกถึง class User ที่​อยู่​ใน project กลาง Ecommerce.Common ถ้า​ทีม Shipping แก้ UpdateAddress() ก็​อาจ​ทำ context Sales พัง​ได้ เพราะ Sales พึ่งพา​พฤติกรรม (behavior) แบบ​เดิม​อยู่

Models Blur — model ที่​เบลอ​เข้าหา​กัน

ถ้า domain ซับซ้อน​พอ​จะ​มี​หลาย context แต่​คุณ​ยัด​ทุก​อย่าง​ไว้​ใน codebase เดียว ขอบเขต​ของ​แต่ละ model จะ ไม่​คง​สภาพ อย่าง​หลีก​เลี่ยง​ไม่​ได้ code จาก context หนึ่ง​จะ​ค่อยๆ ผูก​แน่น​กับ​อีก context หนึ่ง

อย่า​กลัว​ว่า​จะ “ละเมิด DRY” ถ้า code ใน2 context ดู​คล้าย​กัน — บ่อย​ครั้ง code ที่​เริ่มต้น​เหมือน​กัน​จะ​วิวัฒน์​ไป​คนละ​ทาง​ด้วย​เหตุผล​ต่าง​กัน การ​มี code ซ้ำ (duplicate) ที่ loosely coupled นั้น​แทบ​ไม่​เคย​เป็น​ปัญหา ปัญหา​ที่แท้​จริง​คือ​การ couple มัน​เข้า​ด้วย​กัน​ต่างหาก

อีก dependency ที่​ทำให้​ทีม​ช้า​ลง​คือ ฐาน​ข้อมูล​ร่วม สมมติ​ทีม Sales อยาก​แก้ schema ของ User แต่​ไม่มี​ใคร​มั่นใจ​ว่า​จะ​ทำ context Shipping หรือ Billing พัง​ไหม หลาย​ทีม​จึง​ต้อง​หยุด​งาน​สำคัญ​มา​ประสาน​งาน​กัน ยิ่ง​ไป​กว่า​นั้น เมื่อ​หลาย model ใช้ schema ร่วม​กัน (เช่น เฟอร์นิเจอร์ “สั่ง​ทำ” กับ “ผลิต​จำนวน​มาก” ที่​เริ่มต้น​คล้าย​กัน​แต่​ค่อยๆ ต่าง​กัน) column ที่​จำเป็น​กับ model หนึ่ง​จะ​ไร้​ความหมาย​กับ​อีก model — โดย​พื้นฐาน​แล้ว​การ share schema ระหว่าง model ที่​มี semantics ต่าง​กัน​คือ​การ​ละเมิด SRP ที่​มี​ราคา​แพง


วิธี​ที่​นิยม​ที่สุด​ใน​การ​รักษา​ความ​เป็น​อิสระ​ของ context คือ shared-nothing architecture — แต่ละ context มี codebase, datastore, และ​ทีม​เป็น​ของ​ตัวเอง ผลลัพธ์​คือ​ระบบ​ที่​ประกอบ​จาก “แท่ง​แนว​ตั้ง” (vertical) ที่​ตัดขาด​กัน

รูปที่ 11-3: Bounded Context ที่เป็นอิสระด้วย shared-nothing architecture — แต่ละ context มี Web Service, Domain Model, Database และทีมของตัวเอง สื่อสารกันผ่าน contract ที่ชัดเจน (JSON/HTTP) เท่านั้น

เมื่อ context ถูก​แยก​ทาง​กายภาพ นัก​พัฒนา​ใน context หนึ่ง​จะ “เรียก method” ของ​อีก context ตรงๆ ไม่​ได้ และ​เก็บ​ข้อมูล​ใน schema ร่วม​ไม่​ได้ การ​จะ​สร้าง coupling ต้อง​ลงแรง​พิเศษ ซึ่ง​มัก​ทำให้​คน​ยับยั้ง​ชั่งใจ หรือ​ถูก​เพื่อน​ร่วม​ทีม​ท้วงติง​ก่อน การ​แยก​กายภาพ​ที่​ชัดเจน​นี้​ทำให้​แต่ละ context วิวัฒน์​ด้วย “เหตุผล​ภายใน” ของ​มัน​เอง​เท่านั้น

🛒 e-commerce แบบ shared-nothing

ใน​ระบบ​สั่ง​ซื้อ​ของ​เรา context Sales, Billing, และ Shipping ต่าง​มี database ของ​ตัวเอง Sales ไม่​ได้​อ่าน​ตาราง payment ของ Billing ตรงๆ แต่​สื่อสาร​ผ่าน contract ที่​ตกลง​ไว้ (เช่น JSON ผ่าน HTTP หรือ message) เมื่อ​ทีม Billing เปลี่ยน​วิธี​เก็บ​ข้อมูล​ภายใน ก็​ไม่มี​ใคร​พัง​ตาม​ตราบ​ใด​ที่ contract ยัง​เหมือน​เดิม


เมื่อ​ต้อง​เชื่อม​กับ code legacy Eric Evans เสนอ pattern หลาย​ตัว (จาก​เปเปอร์ Getting Started with DDD When Surrounded By Legacy Systems) เพื่อ​จำกัด​ผลกระทบ​ของ legacy ต่อ code ใหม่​ของ​เรา:

  • Bubble ContextBubble Contextcontext เล็ก ๆ ที่​แยก​ตัว​จาก codebase legacy เพื่อ​เป็น “พื้นที่​สะอาด” สำหรับ​สร้าง​และ​วน​ปรับ domain model ใหม่​ตาม​แบบ DDD โดย​ใช้ Anticorruption Layer แปล​ภาษา​ระหว่าง bubble กับ legacy (Eric Evans) มี​แบบ autonomous bubble ที่​มี datastore ของ​ตัวเอง​และ​ซิงค์​แบบ async ด้วยStrategic Design — สร้าง context เล็กๆ ที่​แยก​ตัว​จาก codebase เดิม เพื่อ​เป็น “พื้นที่​สะอาด” สำหรับ​สร้าง domain model ใหม่​และ​วน​ปรับ​ได้​บ่อยๆ ตาม​ที่ DDD ชอบ โดย​มี ACLAnticorruption Layer (ACL)ชั้น​กั้น​ที่​ทีม​ปลาย​น้ำ​สร้าง​ขึ้น​เพื่อ​แปลง model ของ​ระบบ​อื่น​ให้​เป็น​ภาษา​ของ model ตัวเอง ป้องกัน​ไม่​ให้ model ภายนอก​มา “ปน​เปื้อน” model เราStrategic Design เป็น​ชั้น​แปล​ภาษา​ระหว่าง bubble กับ legacy
  • autonomous bubble — ก้าว​ขึ้น​อีก​ขั้น: bubble มี datastore ของ​ตัวเอง รัน​ได้​อิสระ​จาก legacy ใช้ synchronizing ACL ทำการ​ซิงค์​แบบ asynchronous ข้อดี​คือ​ไม่​ต้อง​ไป​แก้ schema ของ legacy แต่​ราคา​คือ​ความ​ซับซ้อน​ของ​การ​ซิงค์​แบบ async ที่​สูง​ขึ้น​มาก
  • เปิด legacy เป็น service — เมื่อ​มี​หลาย context ใหม่​ต้อง​ใช้ legacy ตัว​เดียวกัน การ​ทำ ACL แยก​ต่อ context จะ​แพง​เกิน​ไป ทางออก​คือ​เปิด legacy เป็น HTTP API ที่​คืน JSON คือ pattern Open Host ServiceOpen Host Service (OHS)ทีม​ต้นน้ำ​เปิด​บริการ​เป็น​โพรโทคอล/API ที่​นิยาม​ไว้​ชัดเจน ให้​ทุก​คน​ที่​ต้อง​เชื่อม​ต่อ​มา​ใช้ได้ เหมาะ​กับ​ระบบ​ที่​มี​ผู้​ใช้​หลาย​ฝ่ายStrategic Design นั่นเอง โดย​แต่ละ context ฝั่ง​ผู้​ใช้​ยัง​ต้อง​มี ACL บางๆ แปลกลับ​เป็น model ภายใน​ของ​ตน
เลือก strategy ไหน​ดี?

ทั้ง​สาม​ทาง​เลือก​มี trade-off ต่าง​กัน: bubble พึ่ง legacy เต็ม​ที่​แต่​เริ่ม​ง่าย, autonomous bubble อิสระ​กว่า​แต่​ซิงค์​ยาก, ส่วน​การ​เปิด​เป็น service คุ้ม​เมื่อ​มี​ผู้​ใช้​หลาย​ราย แต่​ต้อง​ไป​แก้ legacy การ​เลือก​ขึ้น​กับ context ของ​คุณ — ไม่มี​คำ​ตอบ​เดียว​ที่​ถูก​เสมอ


ทำไม​ระบบ​สมัย​นี้​ถึง​ต้อง “กระจาย”? เหตุผล​หลัก​คือ scale — server ราคา​ประหยัด​ตัว​เดียว​มัก​รองรับ​ผู้​ใช้​ของ​เว็บ​ยอด​นิยม​ไม่​ไหว ต้อง​กระจาย load ไป​หลาย​เครื่อง (ที่มา​ของ​ยุค cloud) อีก​เหตุผล​คือ fault tolerance — ถ้า​เครื่อง​หนึ่ง​ล่ม เครื่อง​อื่น​ต้อง​รับ​ภาระ​ต่อ​ได้ แต่​การกระจาย​ระบบ​บังคับ​ให้​เรา​ต้อง​แตกมัน​ออก​เป็น​ชิ้น​เล็กๆ ที่ deploy แยก​กัน​ได้ ซึ่ง​ท้าทาย​ความ “ชัดเจน” ของ model ที่ DDD พยายาม​รักษา​ไว้

ระบบ​กระจาย​นำ nonfunctional requirements สาม​ตัว​มา​วาง​บน​โต๊ะ:

  • Scalability — รองรับ load ที่​เพิ่ม​ขึ้น (ผู้​ใช้​พร้อม​กัน​มาก​ขึ้น)
  • Availability — ระบบ​ออนไลน์​และ​พร้อม​ให้​บริการ​บ่อย​แค่​ไหน
  • Reliability — ระบบ​รับมือ​กับ error ได้​ดี​แค่​ไหน
ถาม​ธุรกิจ​ให้​เป็น

อย่า​ถาม​ฝ่ายธุรกิจตรงๆ ว่า “ระบบ​ต้อง scale แค่​ไหน” เพราะ​เขา​ตอบ​ไม่​ได้ ให้​เสนอ​เป็น สถานการณ์​พร้อม​ราคา แทน เช่น “เรา​รับประกัน reliability 99.9999% ได้ แต่​ราคา​จะ​เป็น​สาม​เท่า​ของ​การ​รับประกัน 99.99%” แล้ว​ให้​ฝ่าย​ธุรกิจ​ตัดสิน​ใจ​บน trade-off ที่​จับ​ต้อง​ได้

วิธี​ที่​เข้าถึง​ง่าย​ที่สุด​คือ​ให้ app หนึ่ง “เขียน” ลง database ตำแหน่ง​หนึ่ง แล้ว​อีก app “อ่าน” จาก​ตรง​นั้น เหมาะ​กับ MVP รอบ​แรก​หรือ​ส่วน​ที่​ไม่ critical ด้าน​ประสิทธิภาพ

🛒 Billing poll ตาราง order

Sales ใส่ order ลง​ตาราง Sales ใน SQL ทุก 5 นาที Billing จะ poll ดู​ว่า​มี record ใหม่​ที่ PaymentProcessed = False ไหม ถ้า​เจอ​ก็​ประมวล​ผล​การ​ชำระ​เงิน แล้ว UPDATE ... SET PaymentProcessed = True — ง่าย​และ loosely coupled ใน​ระดับ​หนึ่ง เพราะ​ทั้ง​คู่​คุย​กัน​ผ่าน​การ​อ่าน/เขียน schema เดียวกัน

แต่​ข้อ​เสีย​มา​เร็ว​เมื่อ​ระบบ​โต: ทั้ง​สอง​ระบบ​ผูก​กับ database เดียวกัน เกิด database lock แย่ง​ทรัพยากร​กัน และ database กลาย​เป็น single point of failure (SPOF) ถ้า​มัน​ล่ม​ก็​เจ๊ง​ทั้ง​คู่ ยิ่ง​กว่า​นั้น database integration ไม่​ได้​ชี้นำ​แนวทาง​ที่​ดี​ใน​การ​จัดการ fault เลย — ถ้า Sales crash ก่อน​บันทึก order จะ​หาย​ไหม? ถ้า database ล่ม จะ​รับ order ไม่​ได้​เลย​หรือ? คุณ​ต้อง​คิด​เอง​ทั้งหมด

ส่วน flat file integration (วาง file บน server เช่น Amazon S3 แล้ว​อีก app มา​หยิบ) ก็​คล้าย​กัน — เลี่ยง​ปัญหา lock ได้ แต่​ต้อง​นิยาม format เอง และ​ไม่มี guideline เรื่อง scalability/reliability ให้ ทุก​อย่าง​ต้อง​ลงมือ​ทำ​เอง​ทั้งหมด

RPCRemote Procedure Call (RPC)การ​เรียก method ข้าม​เครือข่าย​แบบ “โปร่งใส” จน​มอง​จาก code ไม่​ออกว่า​เป็น in-memory หรือ​ข้าม​เครื่อง implement ได้​หลาย​แบบ (SOAP, REST, WCF) ข้อดี​คือ code แทบ​ไม่​เปลี่ยน แต่ synchronous RPC มี logical/temporal coupling, resilience ยาก และ scale แพงArchitecture (Remote Procedure Call) มี​เสน่ห์​ตรง​ที่ code แทบ​ไม่​เปลี่ยน — method call ธรรมดา​อาจ​กลาย​เป็นการ​เรียก​ข้าม​เครือข่าย​โดย​มอง​ไม่​ออก​จาก​ตัว code ดู​ตัวอย่าง​นี้:

วาง order ด้วย RPC
var order = salesBoundedContext.CreateOrder(orderRequest);
var paymentStatus = billingBoundedContext.ProcessPaymentFor(order);
if (paymentStatus.IsSuccessful)
{
// อาจเป็น in-memory หรือเป็น HTTP call ข้ามเครื่อง — ดูจาก code ไม่ออก
shippingBoundedContext.ArrangeShippingFor(order);
}

เป้าหมาย​ของ RPC คือ​ทำให้การ​สื่อสาร​ข้าม​เครือข่าย “โปร่งใส” ซึ่ง implement ได้​หลาย​แบบ (SOAP, REST, XML, WCF) และ​ง่าย​กว่า flat file เพราะ​มี framework ช่วย​จัดการ infrastructure ให้ แต่​ความ​โปร่งใส​นี้​แหละ​คือ​จุด​อ่อน​ลึกๆ ของ​มัน

รูปที่ 11-9: ระบบ e-commerce ที่ใช้ synchronous RPC — ทุก call (Store Order, Take Payment, Arrange Shipping) ถูกประมวลผลแบบ synchronous และ block thread จนเสร็จ ผู้ใช้ต้องรอทุก context ทำงานทีละขั้นก่อนได้ response

จุด​อ่อน​สาม​ข้อ​ของ synchronous RPC
  1. Resilience ยาก — RPC ทำให้​คุณ “ลืม” ว่า​เครือข่าย​มี​อยู่ แต่ network error เกิด​ขึ้น​จริง​เสมอ (ดู Fallacies of Distributed Computing) ถ้า payment provider ออฟไลน์ order ก็​ทำ​ไม่​สำเร็จ — เสีย​ทั้ง​ลูกค้า​และ​รายได้ โดย​เฉพาะ​ช่วง peak อย่าง​เทศกาล

  2. Scale แพง — เพราะ Sales, Billing, Shipping ต้อง​ทำงาน​ก่อน​ผู้​ใช้ได้ response ทุก​ตัว​ต้อง​เพิ่ม​ทรัพยากร จะ scale เฉพาะ​เว็บ​อย่าง​เดียว​ไม่​ช่วย

  3. Tight coupling — มี logical coupling (ผู้​เรียก​ผูก​กับ logic ผู้​ถูก​เรียก) และ temporal coupling (ผู้​เรียก​ต้อง​รอ response ทันที) ทำให้ scale แยก​ชิ้น​ไม่​ได้


transaction เป็น best practice ของ​การ​รักษา consistency แต่​ใน​ระบบ​กระจาย transaction มี​ราคา​แพง​มาก​เพราะ​ต้อง​ข้าม​เครือข่าย ตัวอย่าง​คลาสสิก​คือ “จอง​ทริป” ที่​มี​โรงแรม (อยู่​ใน database เรา) + เที่ยว​บิน (อยู่​ระบบ​ภายนอก) เมื่อ​สั่ง​จอง เรา​ล็อก record โรงแรม​ไว้​รอ​จน​จอง​เที่ยว​บิน​เสร็จ — แต่ HTTP call ไป​สาย​การ​บิน​ใช้​เวลา​หลาย​วินาที ทำให้​จำนวน lock และ connection พุ่ง​สูง​จนถึง​จุด​ที่ database ล้ม นี่​คือ​เหตุผล​ที่​ควร​หลีก​เลี่ยง distributed transaction

ทางออก​คือ​ยอมรับ​ว่า Bounded Context ไม่​จำเป็น​ต้อง consistent ตรง​กัน​ตลอด​เวลา แทนที่​จะ rollback ด้วย lock เรา “เดิน​หน้า​ไป​สู่​สถานะ​ที่​แก้​ปัญหา” (roll forward) เช่น ถ้า​จอง​เที่ยว​บิน​ไม่​สำเร็จ ก็​ค่อย​ยกเลิก​โรงแรม​ใน​ภายหลัง โดย​ไม่​ต้อง​อยู่​ใน transaction เดียวกัน นั่น​หมายความ​ว่า order อาจ​มี​อยู่​ใน context หนึ่ง​แต่​ยัง​ไป​ไม่​ถึง​อีก context หนึ่ง — ซึ่ง​ไม่ใช่​เรื่อง​ผิด​ปกติ มัน​คือ Eventual ConsistencyEventual Consistencyความ​ถูกต้อง​ที่ “ตาม​มา​ทีหลัง” ไม่ใช่​ทันที ใช้​กับ​การ​อัปเดต​ข้าม Aggregate ผ่าน Domain Event แทน​การ​บังคับ​ให้​ตรง​กัน​ใน​ทันที​ภายใน transaction เดียวTactical Design

BASE แทน ACID

eventual consistency มา​กับ acronym BASE (Basically Available, Soft state, Eventual consistency) ซึ่ง​ตรง​ข้าม​กับ ACID (Atomicity, Consistency, Isolation, Durability) ที่​เรา​คุ้น​จาก relational database — สอง​คำ​นี้​สะท้อน​ความ​ต่าง​พื้นฐาน​ของ semantics ด้าน consistency

Pat Helland (Life Beyond Distributed Transactions) และ Martin Fowler (Transactionless) เขียน​เรื่อง​นี้​ไว้​ดี​มาก แนะนำ​ให้​อ่าน​ก่อน​สร้าง​ระบบ​จริง

ข้อ​ควร​ระวัง​คือ eventual consistency อาจ​กระทบ user experience — เว็บ​ที่​ให้​สั่ง order ได้​แต่​ไม่​ยืนยัน​ผล​ทันที​อาจ​ทำให้​ผู้​ใช้​บาง​คน​หงุดหงิด แต่​ถ้า​วางแผน​ดี คุณ​จะ​ได้​ประโยชน์​ด้าน scalability พร้อม​รักษา UX ที่​ดี​ไว้​ได้ (Amazon คือ​กรณี​ศึกษา​ที่​ประสบ​ความ​สำเร็จ)


ผู้เชี่ยวชาญ​ด้าน​ระบบ​กระจาย​รู้มากว่า 20 ปี​แล้ว​ว่า synchronous RPC มี​ข้อ​จำกัด และ​มัก​เลือก​ใช้ asynchronous, event-driven messaging แทน แนวคิด reactive (จาก Reactive Manifesto) แทนที่ RPC ด้วย​ข้อความ​แบบ async

รูปที่ 11-10: แทนที่ RPC ด้วย reactive — ผู้ใช้ได้ response (Order Acknowledged) ทันทีที่ระบบรับเจตนาสั่งซื้อ ส่วน Billing และ Shipping ทำงานแบบ async ผ่าน event (Payment Accepted, Shipping Arranged) แล้วส่งอีเมลยืนยันทีหลัง — call ไป 3rd party เท่านั้นที่ยัง synchronous

ใน​รูป 11-10 เมื่อ​ผู้​ใช้​สั่ง​ซื้อ ระบบ​ส่ง message แบบ async ไป​บอก Billing ว่า​มี order แล้ว Billing คุย​กับ payment provider เสร็จ​ก็​ปล่อย event PaymentAccepted ออก​มา ส่วน Shipping subscribe event นั้น​ไว้ พอได้​รับ​ก็​จัด​ส่ง​ทันที ระหว่าง​นั้น​ผู้​ใช้ได้ response กลับ​ทันที​ที่​เจตนา​ถูก​บันทึก แล้ว​ได้​อีเมล​ยืนยัน​ตาม​มา​ภายหลัง — นี่​คือ​การ​แก้​ปัญหา performance ด้วย​การ​ประมวล​ผล​แบบ async การ​สื่อสาร​ข้าม context ด้วย event ลักษณะ​นี้​คือ Integration EventIntegration Eventevent ที่​สื่อสาร “ข้าม” Bounded Context หรือ​ข้าม microservice ผ่าน event bus — ต่าง​จาก Domain Event ที่​ทำงาน​ภายใน context เดียว (in-process)Architecture

ส่วน​ปัญหา resilience แก้​ด้วย​การ​เก็บ​แต่ละ message ไว้​ใน queue จนกว่า​ผู้รับ​จะ​ประมวล​ผล​สำเร็จ ถ้า payment provider มี​ปัญหา message ก็​รอ​ใน queue แล้ว retry เมื่อ​พร้อม ถ้า context ใด crash message ก็​รอ​อยู่​จน​มัน​กลับ​มา — MessagingMessagingการ​สื่อสาร​ระหว่าง Bounded Context แบบ asynchronous ผ่าน​ข้อความ​และ queue เมื่อ message ล้มเหลว​ระบบ retry ได้​ภายหลัง เป็น​รากฐาน​ของ reactive systems ที่​ให้ scalability และ resilience สูง​กว่า RPC แต่​แลก​ด้วย debug ยาก​ขึ้น​และ eventual consistencyArchitecture จึง​ให้​ระดับ resilience ที่​เหนือ​กว่า RPC มาก

🚢 Cargo กับ event แบบ async

ใน​ระบบ​ขนส่ง การ “จอง​การ​ขนส่ง” จะ​ปล่อย Domain EventDomain Eventสิ่ง​ที่ “เกิด​ขึ้น​แล้ว” ใน domain ซึ่ง​ส่วน​อื่น​สนใจ แทน​ด้วย​อ็อบเจ็กต์ที่​เปลี่ยนแปลง​ไม่​ได้ ตั้ง​ชื่อ​เป็น​อดีต เช่น CargoWasRouted ใช้​สื่อสาร​ข้าม AggregateTactical Design เช่น CargoWasRouted ออก​มา context อื่น (เช่น การ​ออก​ใบ​แจ้ง​หนี้​หรือ​การ​แจ้ง​เตือน​ลูกค้า) จะ subscribe event นี้​แล้ว​ทำงาน​ต่อ​แบบ async โดย​ไม่​ต้อง​ให้​ผู้​จอง​รอ ถ้า context ปลายทาง​ล่ม event ก็​จะ​รอ​อยู่​ใน queue — ระบบ​จึง​ทนทาน​ต่อ​ความ​ล้มเหลว​ของ​แต่ละ​ชิ้น​ส่วน

reactive ก็​มี​ราคา

reactive ไม่ใช่​ยา​วิเศษ (silver bullet) มัน​แลก​ความ​ทนทาน​และ scale มา​ด้วย​ต้นทุน: debug ยาก​ขึ้น, code มี indirection มาก​ขึ้น​จน​คน​อื่น​อ่าน​เข้าใจ​ยาก, ต้อง​รับมือ eventual consistency, และ​มี infrastructure component เพิ่ม (ตัว​ส่ง/retry message) ทีม​จึง​ต้อง​ผ่าน learning curve ที่​ชัน​กว่า RPC

มี — RPC คือ​เครื่องมือ​ที่​อาจ​เหมาะ​ที่สุด​ใน​บาง​สถานการณ์ เมื่อ scalability/reliability ไม่ใช่​ข้อ​จำกัด​หลัก:

  • Time-to-market — นัก​พัฒนา​คุ้น​เคย​กับ HTTP มากกว่า messaging จึง​หา​คน​และ​สร้าง​งาน​ได้​เร็ว​กว่า เหมาะ​กับ​การ​รีบ​ออก feature ไป​ทดสอบ​ตลาด
  • หา​คน​และ​ฝึก​อบรม​ง่าย​กว่า — แทบ​ไม่มี​นัก​พัฒนา​ที่​ไม่รู้จัก HTTP
  • Platform decoupled — messaging framework หลาย​ตัว​ข้าม platform ได้​ยาก ส่วน HTTP สื่อสาร​ข้าม​ภาษา/OS ได้​อย่าง​ราบรื่น
  • External integration — การ​คุย​กับ​บริการ​ภายนอก (payment provider) หรือ​ให้​คน​อื่น​มา​เชื่อม​ระบบ​เรา แทบ​ทั้งหมด​ใช้ REST/RPC ผ่าน HTTP เพราะ​ทุก​คน​ทำงาน​กับ HTTP ได้

Service Oriented ArchitectureService Oriented Architecture (SOA)สถาปัตยกรรม​สำหรับ​สร้าง software service ที่ business-oriented และ loosely coupled หัวใจ​คือ loose coupling (ไม่ใช่​แค่ web service) เมื่อ​มอง Bounded Context เป็น SOA service จะ map context ระดับ​สูง​ลง​สู่ business component และ component ที่ event-driven ได้Architecture (SOA) คือ​สถาปัตยกรรม​สำหรับ​สร้าง software service ที่ business-oriented และ loosely coupled ถ้า​มอง Bounded Context เป็น SOA service เรา​จะ map context ระดับ​สูง​ลง​สู่ software component ระดับ​ล่าง​ที่ event-driven ได้ — ได้​ทั้ง​ประโยชน์​ด้าน business alignment ของ SOA และ scalability/resilience ของ reactive

SOA ไม่​ได้​แปล​ว่า web service

SOA กลาย​เป็น​คำ​กำกวม​ใน​วงการ Netflix นิยาม service ไว้​เล็ก​มาก ขณะ​ที่​หลาย​บริษัท​ใช้ service ขนาด​ใหญ่​กว่า และ​ไม่​จำเป็น​ต้อง​ใช้ message bus เสมอ​ไป แต่ หัวใจ​ของ SOA คือ loose coupling เพื่อ​ประโยชน์​ทาง​ธุรกิจ​และ​เทคนิค ไม่ใช่​แค่ “การ​มี web service”

SOA ชี้นำ​ให้​เรา​แตก context ลง​เป็น​ลำดับ​ชั้น:

graph TD
  BC["Shipping Bounded Context"] --> BC1["Priority Shipping<br/>(Business Component)"]
  BC --> BC2["Standard Shipping<br/>(Business Component)"]
  BC1 --> C1["Arrange Shipping<br/>(Component)"]
  BC1 --> C2["Cancel Shipping<br/>(Component)"]
  BC2 --> C3["Arrange Shipping<br/>(Component)"]
  BC2 --> C4["Cancel Shipping<br/>(Component)"]
  • Business Component — แยก​แต่ละ​ความ​รับผิดชอบ​หลัก​ของ context ออก​มา (เช่น Priority vs Standard shipping) ทำให้​คุย​กับ​ธุรกิจ​ชัด​ขึ้น​และ code single responsibility กฎ​สำคัญ: business component ไม่ share dependency กัน ห้าม RPC หา​กัน​หรือ​ใช้ database เดียวกัน
  • Component — แตก business component ลง​ไป​อีก (เช่น Arrange Shipping vs Cancel Shipping) เป็น หน่วย​ของ​การ deploy ทำให้​จัดสรร hardware ตาม​ความ​สำคัญ​ทาง​ธุรกิจ​ได้​ละเอียด — เช่น​วาง Arrange Shipping บน​เครื่อง bare-metal แรงๆ ส่วน Cancel Shipping บน VM เล็กๆ

ข้อ​แตก​ต่าง​สำคัญ: component share dependency กัน​ได้​ภายใน business component เดียวกัน (เช่น database ร่วม) เพราะ​มัน​ทำงาน​ใกล้​ชิด​กัน​มาก การ share จึง​เพิ่ม cohesion โดย​ไม่​สร้าง​ปัญหา — ต่าง​จาก business component กับ context ที่​ห้าม share

Micro Service Architecture (MSA)

การ​ทำ​ตาม​แนวทางใน​บท​นี้​จะ​ใกล้​เคียง​กับ MSA มาก โดย​แต่ละ component เทียบ​ได้​กับ1 micro service แต่ MSA เข้มงวด​กว่า: micro service ต้อง autonomous อย่าง​เต็ม​ที่ มี database ของ​ตัวเอง และ​สื่อสาร​ผ่าน event แบบ publish/subscribe (เพราะ command/RPC สร้าง coupling) Fred George และ Martin Fowler เป็น​ผู้​ผลักดัน​แนวคิด​นี้ — บาง​คน​ถึง​กับ​บอกว่า micro service ควร​มี code น้อย​กว่า​พัน​บรรทัด

สรุป​แก่น​ของ​บท

ปัจจุบัน app ส่วน​ใหญ่​คือ distributed system การ​ใช้ DDD ยัง​ให้​ประโยชน์ แต่​มา​พร้อม​ความ​ท้าทาย​ทั้ง​ด้าน​เทคนิค (scalability, reliability) และ​ด้าน​สังคม (การ​จัด​ทีม, การ​ส่ง​มอบ​งาน​ให้​เร็ว) — database integration ตั้ง​ค่า​ได้​ง่าย​แต่​ไม่​เหมาะ​กับ​งาน high-scale ส่วน RPC กับ messaging คือ​สอง​แนวทาง​หลัก​ที่​แตก​ต่าง​กัน​โดย​สิ้นเชิง การ​มอง Bounded Context เป็น SOA service แล้ว​ผสาน​กับ reactive programming ช่วย​ให้​จัดสรร infrastructure ตาม​ความ​สำคัญ​ทาง​ธุรกิจ​ได้ และ​จัด​ทีม​ให้​ตรง​กับ context เพื่อ​ลด overhead ของ​การ​สื่อสาร


🔗 อ้างอิง​เพิ่มเติม​ใน DevIQ

เจาะ​ลึก​แนวคิด​ใน​บท​นี้​ต่อ​ได้ที่​คลัง​อ้างอิง DevIQ:

  • Anti-Corruption Layer — ชั้น​แปล​ภาษา​ที่​บท​นี้​ใช้​กัน code ใหม่​จาก legacy ใน Bubble Context และ autonomous bubble ไม่​ให้ model legacy รั่ว​เข้า​มา​ปน​กับ domain ของ​เรา
  • Domain Events — กลไก​เบื้องหลัง Integration Event ที่​ใช้​สื่อสาร​ข้าม Bounded Context แบบ asynchronous ใน​แนวทาง event-driven reactive DDD ของ​บท​นี้
  • Single Responsibility Principle — บท​นี้​อ้างถึงตรงๆ ว่าการ​แยก context ให้ loosely coupled สอดคล้อง​กับ SRP และ​การ share schema ระหว่าง model ที่ semantics ต่าง​กัน​คือ​การ​ละเมิด SRP ที่​มี​ราคา​แพง
  • Conway’s Law — อธิบาย​ว่า​ทำไม​การ​จัด​ทีม​ให้​ตรง​กับ Bounded Context จึง​ลด overhead การ​สื่อสาร ซึ่ง​เป็น “ด้าน​สังคม” ของ​การ​เชื่อม context ที่​บท​นี้​เน้น​ย้ำ​คู่​กับ​ด้าน​เทคนิค

เช็กความเข้าใจ — บทที่ 11

ข้อ 1 / 4

ทำไม shared-nothing architecture จึงช่วยรักษาความเป็นอิสระของ Bounded Context?