รู้จักการเชื่อม 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 พุ่งสูงสุด
Bounded Context ควรเป็นอิสระต่อกัน (autonomous)
หัวข้อที่มีชื่อว่า “Bounded Context ควรเป็นอิสระต่อกัน (autonomous)”เมื่อระบบโตขึ้น 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
กับดักของการอยู่ codebase เดียวกัน
หัวข้อที่มีชื่อว่า “กับดักของการอยู่ codebase เดียวกัน”การวาง context ทั้งหมดไว้ใน solution เดียวมีข้อดีคือช่วยให้นักพัฒนาเห็น “ภาพใหญ่” ว่ามีโลกนอก context ของตัวเอง แต่ก็เพิ่มความเสี่ยงที่จะเกิด dependency ระหว่างกัน ลองนึกถึง class User ที่อยู่ใน project กลาง Ecommerce.Common ถ้าทีม Shipping แก้ UpdateAddress() ก็อาจทำ context Sales พังได้ เพราะ Sales พึ่งพาพฤติกรรม (behavior) แบบเดิมอยู่
ถ้า domain ซับซ้อนพอจะมีหลาย context แต่คุณยัดทุกอย่างไว้ใน codebase เดียว ขอบเขตของแต่ละ model จะ ไม่คงสภาพ อย่างหลีกเลี่ยงไม่ได้ code จาก context หนึ่งจะค่อยๆ ผูกแน่นกับอีก context หนึ่ง
อย่ากลัวว่าจะ “ละเมิด DRY” ถ้า code ใน2 context ดูคล้ายกัน — บ่อยครั้ง code ที่เริ่มต้นเหมือนกันจะวิวัฒน์ไปคนละทางด้วยเหตุผลต่างกัน การมี code ซ้ำ (duplicate) ที่ loosely coupled นั้นแทบไม่เคยเป็นปัญหา ปัญหาที่แท้จริงคือการ couple มันเข้าด้วยกันต่างหาก
Integration ผ่าน database — และทำไมมันเจ็บ
หัวข้อที่มีชื่อว่า “Integration ผ่าน database — และทำไมมันเจ็บ”อีก dependency ที่ทำให้ทีมช้าลงคือ ฐานข้อมูลร่วม สมมติทีม Sales อยากแก้ schema ของ User แต่ไม่มีใครมั่นใจว่าจะทำ context Shipping หรือ Billing พังไหม หลายทีมจึงต้องหยุดงานสำคัญมาประสานงานกัน ยิ่งไปกว่านั้น เมื่อหลาย model ใช้ schema ร่วมกัน (เช่น เฟอร์นิเจอร์ “สั่งทำ” กับ “ผลิตจำนวนมาก” ที่เริ่มต้นคล้ายกันแต่ค่อยๆ ต่างกัน) column ที่จำเป็นกับ model หนึ่งจะไร้ความหมายกับอีก model — โดยพื้นฐานแล้วการ share schema ระหว่าง model ที่มี semantics ต่างกันคือการละเมิด SRP ที่มีราคาแพง
ใช้เส้นแบ่งทางกายภาพบังคับให้ model สะอาด
หัวข้อที่มีชื่อว่า “ใช้เส้นแบ่งทางกายภาพบังคับให้ model สะอาด”วิธีที่นิยมที่สุดในการรักษาความเป็นอิสระของ context คือ shared-nothing architecture — แต่ละ context มี codebase, datastore, และทีมเป็นของตัวเอง ผลลัพธ์คือระบบที่ประกอบจาก “แท่งแนวตั้ง” (vertical) ที่ตัดขาดกัน

เมื่อ context ถูกแยกทางกายภาพ นักพัฒนาใน context หนึ่งจะ “เรียก method” ของอีก context ตรงๆ ไม่ได้ และเก็บข้อมูลใน schema ร่วมไม่ได้ การจะสร้าง coupling ต้องลงแรงพิเศษ ซึ่งมักทำให้คนยับยั้งชั่งใจ หรือถูกเพื่อนร่วมทีมท้วงติงก่อน การแยกกายภาพที่ชัดเจนนี้ทำให้แต่ละ context วิวัฒน์ด้วย “เหตุผลภายใน” ของมันเองเท่านั้น
ในระบบสั่งซื้อของเรา context Sales, Billing, และ Shipping ต่างมี database ของตัวเอง Sales ไม่ได้อ่านตาราง payment ของ Billing ตรงๆ แต่สื่อสารผ่าน contract ที่ตกลงไว้ (เช่น JSON ผ่าน HTTP หรือ message) เมื่อทีม Billing เปลี่ยนวิธีเก็บข้อมูลภายใน ก็ไม่มีใครพังตามตราบใดที่ contract ยังเหมือนเดิม
การเชื่อมกับระบบ legacy
หัวข้อที่มีชื่อว่า “การเชื่อมกับระบบ legacy”เมื่อต้องเชื่อมกับ 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 ภายในของตน
ทั้งสามทางเลือกมี trade-off ต่างกัน: bubble พึ่ง legacy เต็มที่แต่เริ่มง่าย, autonomous bubble อิสระกว่าแต่ซิงค์ยาก, ส่วนการเปิดเป็น service คุ้มเมื่อมีผู้ใช้หลายราย แต่ต้องไปแก้ legacy การเลือกขึ้นกับ context ของคุณ — ไม่มีคำตอบเดียวที่ถูกเสมอ
การเชื่อม Bounded Context แบบกระจาย (distributed)
หัวข้อที่มีชื่อว่า “การเชื่อม Bounded Context แบบกระจาย (distributed)”ทำไมระบบสมัยนี้ถึงต้อง “กระจาย”? เหตุผลหลักคือ scale — server ราคาประหยัดตัวเดียวมักรองรับผู้ใช้ของเว็บยอดนิยมไม่ไหว ต้องกระจาย load ไปหลายเครื่อง (ที่มาของยุค cloud) อีกเหตุผลคือ fault tolerance — ถ้าเครื่องหนึ่งล่ม เครื่องอื่นต้องรับภาระต่อได้ แต่การกระจายระบบบังคับให้เราต้องแตกมันออกเป็นชิ้นเล็กๆ ที่ deploy แยกกันได้ ซึ่งท้าทายความ “ชัดเจน” ของ model ที่ DDD พยายามรักษาไว้
ระบบกระจายนำ nonfunctional requirements สามตัวมาวางบนโต๊ะ:
- Scalability — รองรับ load ที่เพิ่มขึ้น (ผู้ใช้พร้อมกันมากขึ้น)
- Availability — ระบบออนไลน์และพร้อมให้บริการบ่อยแค่ไหน
- Reliability — ระบบรับมือกับ error ได้ดีแค่ไหน
อย่าถามฝ่ายธุรกิจตรงๆ ว่า “ระบบต้อง scale แค่ไหน” เพราะเขาตอบไม่ได้ ให้เสนอเป็น สถานการณ์พร้อมราคา แทน เช่น “เรารับประกัน reliability 99.9999% ได้ แต่ราคาจะเป็นสามเท่าของการรับประกัน 99.99%” แล้วให้ฝ่ายธุรกิจตัดสินใจบน trade-off ที่จับต้องได้
Database & Flat File Integration
หัวข้อที่มีชื่อว่า “Database & Flat File Integration”วิธีที่เข้าถึงง่ายที่สุดคือให้ app หนึ่ง “เขียน” ลง database ตำแหน่งหนึ่ง แล้วอีก app “อ่าน” จากตรงนั้น เหมาะกับ MVP รอบแรกหรือส่วนที่ไม่ critical ด้านประสิทธิภาพ
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 ให้ ทุกอย่างต้องลงมือทำเองทั้งหมด
RPC — เรียก method ข้ามเครือข่ายแบบโปร่งใส
หัวข้อที่มีชื่อว่า “RPC — เรียก method ข้ามเครือข่ายแบบโปร่งใส”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 ดูตัวอย่างนี้:
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 ให้ แต่ความโปร่งใสนี้แหละคือจุดอ่อนลึกๆ ของมัน

-
Resilience ยาก — RPC ทำให้คุณ “ลืม” ว่าเครือข่ายมีอยู่ แต่ network error เกิดขึ้นจริงเสมอ (ดู Fallacies of Distributed Computing) ถ้า payment provider ออฟไลน์ order ก็ทำไม่สำเร็จ — เสียทั้งลูกค้าและรายได้ โดยเฉพาะช่วง peak อย่างเทศกาล
-
Scale แพง — เพราะ Sales, Billing, Shipping ต้องทำงานก่อนผู้ใช้ได้ response ทุกตัวต้องเพิ่มทรัพยากร จะ scale เฉพาะเว็บอย่างเดียวไม่ช่วย
-
Tight coupling — มี logical coupling (ผู้เรียกผูกกับ logic ผู้ถูกเรียก) และ temporal coupling (ผู้เรียกต้องรอ response ทันที) ทำให้ scale แยกชิ้นไม่ได้
Distributed transaction กับ Eventual Consistency
หัวข้อที่มีชื่อว่า “Distributed transaction กับ Eventual Consistency”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
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 คือกรณีศึกษาที่ประสบความสำเร็จ)
Event-Driven Reactive DDD
หัวข้อที่มีชื่อว่า “Event-Driven Reactive DDD”ผู้เชี่ยวชาญด้านระบบกระจายรู้มากว่า 20 ปีแล้วว่า synchronous RPC มีข้อจำกัด และมักเลือกใช้ asynchronous, event-driven messaging แทน แนวคิด reactive (จาก Reactive Manifesto) แทนที่ RPC ด้วยข้อความแบบ async

ในรูป 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 มาก
ในระบบขนส่ง การ “จองการขนส่ง” จะปล่อย Domain EventDomain Eventสิ่งที่ “เกิดขึ้นแล้ว” ใน domain ซึ่งส่วนอื่นสนใจ แทนด้วยอ็อบเจ็กต์ที่เปลี่ยนแปลงไม่ได้ ตั้งชื่อเป็นอดีต เช่น CargoWasRouted ใช้สื่อสารข้าม AggregateTactical Design เช่น CargoWasRouted ออกมา context อื่น (เช่น การออกใบแจ้งหนี้หรือการแจ้งเตือนลูกค้า) จะ subscribe event นี้แล้วทำงานต่อแบบ async โดยไม่ต้องให้ผู้จองรอ ถ้า context ปลายทางล่ม event ก็จะรออยู่ใน queue — ระบบจึงทนทานต่อความล้มเหลวของแต่ละชิ้นส่วน
reactive ไม่ใช่ยาวิเศษ (silver bullet) มันแลกความทนทานและ scale มาด้วยต้นทุน: debug ยากขึ้น, code มี indirection มากขึ้นจนคนอื่นอ่านเข้าใจยาก, ต้องรับมือ eventual consistency, และมี infrastructure component เพิ่ม (ตัวส่ง/retry message) ทีมจึงต้องผ่าน learning curve ที่ชันกว่า RPC
แล้ว RPC ยังมีที่ทางไหม?
หัวข้อที่มีชื่อว่า “แล้ว 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 ได้
SOA และ Reactive DDD
หัวข้อที่มีชื่อว่า “SOA และ Reactive DDD”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 กลายเป็นคำกำกวมในวงการ 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
การทำตามแนวทางในบทนี้จะใกล้เคียงกับ 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:
- 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?