บทที่ 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.go packages
  • 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 ส่งมอบ:

  1. import allow/deny matrix
  2. module public API list
  3. adapter ownership decision
  4. composition root pseudocode
  5. three automated checks
  6. 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

อ่านเพิ่มเติม