บทที่ 16 · Part 3 — Organizing the Codebase
Hybrid Modular Architecture
ใช้ business capability เป็นโครงนอกและ architectural roles ภายใน slice พร้อม composition root และ import rules
Hybrid Modular Architecture
เมื่อ codebase โต ทีมมักเลือกระหว่าง “Vertical Slice ทั้งหมด” กับ “Clean Architecture ทั้งระบบ” ราวกับใช้ร่วมกันไม่ได้ ในทางปฏิบัติ business capability เหมาะเป็นโครงนอก ส่วน module ที่ซับซ้อนเลือก roles ภายในของตนได้ Hybrid modular architecture ทำให้ Payment rich โดยไม่บังคับ Notification มี layer เท่ากัน
จบบทนี้คุณจะ
ออกแบบ composition root, business modules, local/provider adapters และ platform code กำหนด public module contracts กับ import direction และสร้าง architecture fitness checks โดยรู้ว่า path names เป็น course convention ไม่ใช่มาตรฐาน DDD/Go
[!IMPORTANT] วิธีอ่านหลักฐานในบทนี้ คำที่เป็น Definition/Pattern มีแหล่งต้นทางอยู่ใกล้ claim ส่วน decision table, starting structure, checklist และ lab เป็น Heuristic/Trade-off หรือ Course Convention สำหรับฝึกตัดสินใจ ไม่ใช่มาตรฐานสากล เว้นแต่บทจะระบุแหล่งและขอบเขตไว้ชัดเจน
โครงสร้างชั้นนอก
ตัวอย่าง:
cmd/
api/main.go
worker/main.go
internal/
app/
bootstrap.go
modules/
customer/
payment/
domain.go
ports.go
service.go
http.go
workflow.go
repository/mysql/
notification/
adapters/
paymentgateway/stripe/
platform/
observability/
clock/
หลักคือ outer grouping บอก business vs external/platform responsibilities
ภายใน customer, payment, notification ไม่จำเป็นต้องมี tree เดียวกัน
cmd เลือก binary และ wiring app มี bootstrap/composition policies ที่ข้าม modules
เท่าที่จำเป็น แต่ห้ามกลายเป็น global business service
Adapter ใน Module หรือ Integration ร่วม
วาง adapter ใต้ module เมื่อมี consumer เดียวและภาษาเฉพาะ:
modules/tax/adapter/vendorx/
วาง integration adapter กลางเมื่อหลาย modules consume provider capability ผ่าน public contract ที่ตั้งใจและมี owner เช่น payment gateway connectivity:
adapters/paymentgateway/stripe/
แม้อยู่กลาง แต่ port ยังควรถูก define โดย consumer/application contract อาจมี integration module ซึ่งเผย stable application-owned facade หากหลาย consumer มี language เดียวจริง อย่าให้ shared adapter คืน Stripe DTO
Contract ของ Module
ใน modular monolith direct function call ผ่าน exported API มักง่ายและ reliable กว่า การใช้ internal message bus ทุกอย่าง Contract อาจเป็น:
- command method สำหรับ request ที่ต้องรู้ outcome
- query interface สำหรับ owned read
- domain/integration event สำหรับ decoupled reaction
- Published Language DTO ที่ version/owner ชัด
ห้าม peer module import repository/mysql, unexported domain representation หรือ query
table โดยตรง ถ้า Customer ต้องข้อมูล Billing ให้ผ่าน query contract หรือ replicated read
model ตาม freshness requirement
กฎ Dependency
เขียน rules เป็นข้อความก่อน automate:
modules may not import another module's internal adapter or repository
domain files may not import net/http, database/sql, Redis, or vendor SDKs
provider adapters may depend on consumed port contracts
platform may not depend on business modules
cmd/app may depend on all modules only for composition
cross-module writes must use owner contract, never direct SQL
Go internal ช่วยขอบเขตภายนอก repository แต่ไม่ป้องกัน sibling modules ภายในทั้งหมด
จึงใช้ static checker, go list -deps, custom script หรือ dependency test เพิ่มได้
Fitness check ต้องสะท้อน risk จริง ห้ามบังคับชื่อไฟล์ทุกตัว ตัวอย่าง valuable checks:
- reject imports ของ Stripe packages นอก adapter path
- reject cycles ข้าม modules
- reject
database/sqlในdomain.gopackages - scan SQL ownership prefix หรือ enforce DB grants ใน integration environment
Shared Platform ที่ไม่กลายเป็นที่ทิ้งรวม
Platform module ควร deep และมี contract แคบ เช่น clock.Clock, ID generation,
tracing bootstrap หรือ transaction runner ที่ไม่ซ่อน semantics หลีกเลี่ยง wrapper
รอบ standard library ที่ไม่มี policy
Observability helper ควรรับ attributes ที่ safe และไม่รู้ Customer/Invoice models หาก platform import business modules dependency direction กลับด้าน
Shared transaction ไม่ใช่ข้ออ้างให้ละลาย ownership
Modular monolith อาจใช้ MySQL transaction เดียวในบาง use case แต่ cross-module atomic write ทำให้ extraction และ ownership ยาก ต้องบันทึกว่าใคร orchestrate, invariant ใด ต้อง atomic และเหตุใด contract แยกไม่พอ ห้ามให้ module เขียน table กันโดยสะดวก
แบบฝึกปฏิบัติ: ทบทวน Folder Tree
นำ teaching tree ข้างต้นแล้วเพิ่ม Billing modules, Portal query และ Redis read model ส่งมอบ:
- import allow/deny matrix
- module public API list
- adapter ownership decision
- composition root pseudocode
- three automated checks
- exception process พร้อม expiry date
จากนั้น trace ApproveRefund ว่า HTTP Principal, application authorization, Charge
invariant, MySQL repository และ gateway adapter อยู่ตำแหน่งใด หากมี business decision
ใน app/bootstrap หรือ Stripe adapter ให้ย้ายกลับ owner
รายการตรวจสอบ
- Business capability เป็น outer structure
- แต่ละ module เลือก internal architecture ตาม force
- Composition root รู้ concrete types แต่ไม่ถือ business rules
- Provider adapter คืน application-owned outcomes
- Platform code ไม่มี business dependency
- Cross-module contract/ownership ถูก enforce มากกว่า folder convention
- Fitness checks เน้น dependency และ vendor/data containment
- Exception มี owner, reason และ review date
สรุปบทนี้
Hybrid modular architecture รวม slice-based outer structure กับ roles ภายในอย่างพอดี
มันไม่ใช่ template สากล ชื่อ modules/adapters/platform เป็นเพียง course convention
คุณภาพจริงอยู่ที่ public contracts, import direction, data ownership และ executable checks
อ่านเพิ่มเติม
- Go module layout — official module/project layout
- Hexagonal Architecture — adapter boundary และ testability
- Building Evolutionary Architectures — fitness functions
- Monolith First — modular evolution และ trade-offs