บทที่ 14 · Part 3 — Organizing the Codebase

Large Multi-Module Codebase: Billing

แบ่ง Billing ด้วย capability, lifecycle, invariant และ ownership พร้อม contracts และคำถาม boundary ที่ยังไม่ปิด

Large Multi-Module Codebase: Billing

Billing repository โตจนมี Invoice, Receipt, Credit Note, Subscription, Customer, Meter, Seller และ Tax ทีมพยายามแบ่ง module ตาม table 1 table ต่อ package ผลคือ use case Issue Invoice ต้อง import 7 packages และไม่มีใครแน่ใจว่าใครเป็นเจ้าของ amount due การแบ่งที่ดีต้องเริ่มจาก capability, lifecycle, invariant และ ownership

จบบทนี้คุณจะ

สร้าง Billing module map แยก subscriptions, metering, invoicing, receivables, receipts และ tax ด้วยหลักฐาน ตัดสินความสัมพันธ์ Invoice/Credit Note และบทบาท Customer/Seller พร้อม public contracts กับ unresolved questions โดยอนุญาตแต่ละ module ใช้ architecture ต่างกัน

[!IMPORTANT] วิธีอ่านหลักฐานในบทนี้ คำที่เป็น Definition/Pattern มีแหล่งต้นทางอยู่ใกล้ claim ส่วน decision table, starting structure, checklist และ lab เป็น Heuristic/Trade-off หรือ Course Convention สำหรับฝึกตัดสินใจ ไม่ใช่มาตรฐานสากล เว้นแต่บทจะระบุแหล่งและขอบเขตไว้ชัดเจน

เริ่มจาก Capability และเส้นทางธุรกิจ

Candidate modules มีคำถามเจ้าของต่างกัน:

CandidateOutcome / decision ที่อาจเป็นเจ้าของ
Subscriptionscontract lifecycle, plan change, billing cadence
Meteringingest usage, deduplicate, aggregate measurement
Invoicingissue immutable commercial document, line calculation, lifecycle
Receivablesoutstanding obligation, collection, allocation, ageing
Receiptsevidence/record of payment received ตาม policy
Taxtax determination/result integration หรือ snapshot

ชื่อเหล่านี้ยังเป็น hypotheses ให้ trace journey Usage → Invoice → Collection → Receipt แล้วดูว่า language, event และ policy เปลี่ยนที่ใด

Invoice และ Credit Note

Credit Note เกี่ยวข้องกับ Invoice แต่คำถามคือ relation แบบใด:

  • หาก Credit Note เป็น correction document ใน lifecycle เดียว ใช้ numbering/audit policy ร่วม และ Invoicing owner รับผิดชอบ อาจอยู่ module เดียว
  • หาก correction มี workflow/approval/regulatory model แยกและทีมแยก อาจเป็น module ย่อยหรือ boundary อื่น
  • ไม่ควร mutate issued Invoice ย้อนหลังเพียงเพราะ database update ง่ายกว่า หาก business policy กำหนด correction document

เอกสารทางบัญชีและภาษีต้องอ้าง policy จริง

คอร์สไม่กำหนดว่า Invoice ต้อง immutable, numbering แบบใด หรือ Credit Note ใช้เมื่อใด ระบบจริงต้องให้ Accounting/Tax/Legal owner ระบุ jurisdiction, เอกสาร, effective date และ audit requirements ก่อนออกแบบ invariant

ใน lab ให้ใช้ invariant สมมติแบบมีป้าย เช่น “เอกสารที่ ISSUED เปลี่ยน line ไม่ได้; correction สร้าง linked adjustment document” แล้วระบุว่ายังต้อง validate กับ owner

Customer กับ Seller ขึ้นกับ Context

Billing ไม่จำเป็นต้อง own enterprise Customer/Seller master ทั้งหมด สิ่งที่ Invoice ต้องใช้ อาจเป็น immutable billing-party snapshot ณ เวลาออกเอกสาร:

type BillingPartySnapshot struct {
	ExternalPartyID string
	LegalName       string
	TaxID           string
	Address         PostalAddress
}

snapshot มี purpose เพื่อ historical document ไม่ใช่ cache สำหรับ authorization Customer master owner ส่ง Published Language หรือ Billing query แล้วเก็บ version ที่ใช้ Seller อาจเป็น legal entity configuration ของ Billing หาก policy/lifecycle อยู่ที่นี่จริง

ถามเสมอ:

  • ใครแก้ legal identity และใครรับ correction
  • เอกสารเก่าต้องเปลี่ยนตาม master data หรือคง snapshot
  • field ใดใช้ display field ใดใช้ decision
  • data retention/access owner คือใคร

แผนที่ Module เบื้องต้น

แต่ละเส้นต้องตัดสิน command/event/query และ idempotency ตัวอย่าง InvoiceIssued ควรมี invoice ID, version/schema, monetary totals/currency และ occurred time ตาม contract แต่ไม่ควรเผย persistence row ทั้งก้อน

ต่าง Module ต่าง Architecture

  • Meter ingestion อาจใช้ Transaction Script + idempotent store เพราะ flow หนักที่ throughput
  • Invoicing อาจใช้ Rich Domain Model สำหรับ document lifecycle/line invariants
  • Tax อาจเป็น Ports & Adapters รอบ external engine และ snapshot result
  • Receivables อาจใช้ application workflow + domain policies
  • Portal query ใช้ read model ไม่ต้อง hydrate Aggregates

ความสม่ำเสมอที่ต้องรักษาคือ boundary contract, observability, security และ engineering standards ไม่ใช่บังคับ tactical pattern เดียวทุก module

Contract ที่ Module เปิดให้ใช้ และ Data Ownership

สร้าง registry:

Module: Invoicing
Owns: invoice lifecycle, line/totals policy, document identifiers
Consumes: BillingSchedule, UsageFact, TaxResult, BillingPartySnapshot
Publishes: InvoiceIssued, InvoiceAdjusted
Authoritative tables: billing.invoices, billing.invoice_lines
Does not own: collection attempt, provider payment, customer master
Architecture: rich model + application services + MySQL repository
Open questions: correction rules and tax owner approval

ห้าม module อื่นเขียน table ของ Invoicing โดยตรง แม้ใช้ MySQL instance เดียวกัน enforce ผ่าน DB grants/schema, package visibility และ tests ตามความเสี่ยง

แบบฝึกปฏิบัติ: หา Billing Boundary

ทำ module cards สำหรับ candidates ทั้ง 6 แต่ละ card มี language, commands, events, invariants, owner, authoritative data, architecture และ open questions จากนั้น trace:

  1. late usage มาหลัง Invoice issued
  2. partial payment 2 ครั้ง
  3. customer เปลี่ยน legal name
  4. collection succeeded แต่ receipt creation ล้มเหลว

หากคำตอบต้องแก้ table ข้าม module ให้ทบทวน contract/ownership อย่ารีบ merge module เพราะ workflow ข้าม boundary เป็นเรื่องปกติ

รายการตรวจสอบ

  • Module แบ่งตาม capability/lifecycle/invariant ไม่ใช่ table
  • Invoice/Credit Note relation มี policy evidence หรือ open question
  • Customer/Seller ระบุ owner และ snapshot semantics
  • แต่ละ contract บอก fact/intent และ versioning responsibility
  • Module ใช้ architecture ต่างกันได้พร้อมเหตุผล
  • Direct table writes ข้าม owner ถูกห้าม
  • Accounting/tax assumptions มี owner/date ไม่ถูกทำเป็น universal rule

สรุปบทนี้

Billing ใหญ่ควรถูกแบ่งด้วยภาษา กฎ lifecycle และ ownership รายชื่อ noun เป็นเพียง candidate map Invoice, Receipt และ Credit Note อาจสัมพันธ์กันแต่ไม่จำเป็นต้องเป็น model เดียว Customer/Seller มักต้องแยก master ownership กับ Billing snapshot และแต่ละ module เลือก code architecture ตาม force ของตัวเอง

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