Scale-Ready Architecture: Modular Monolith ที่แยกได้ตั้งแต่วันแรก
Scale-Ready Architecture
Part 1: หลักการออกแบบระบบที่ไม่ต้องเลือกระหว่างความเร็วกับความยืดหยุ่น
ทุกทีมที่เคยเขียน Monolith มาถึงจุดเดียวกัน
ช่วงแรก ทุกอย่างเร็ว deploy ง่าย ทีมเล็กทำงานได้คล่อง แต่พอระบบโต ทีมขยาย หรือ traffic พุ่ง — คำถามเดิมก็ตามมาว่า “ถึงเวลาแยก Microservice แล้วหรือยัง?”
แล้วถ้าแยกเร็วเกินไป ก็เจอปัญหาชุดใหม่ทันที ทั้ง distributed system overhead, network latency, และ deployment complexity ที่ทีมเล็กแบกไม่ไหว
Architecture ใน series นี้ตอบคำถามนั้นต่างออกไป — ไม่ต้องเลือก
ออกแบบให้เริ่มต้นเป็น Monolith ที่ deploy และ operate ได้ถูก แต่มีโครงสร้างที่พร้อมแยกเป็น Microservice ได้ทุกเมื่อ โดยไม่ต้อง refactor ครั้งใหญ่ เปลี่ยนแค่ 1 บรรทัดใน app.ts — เท่านั้น
Prerequisites — ควรรู้อะไรก่อนอ่าน
Series นี้ใช้แนวคิดหลายตัวพร้อมกัน ถ้ายังไม่คุ้นกับตัวไหน แนะนำให้อ่านทำความเข้าใจก่อน
| แนวคิด | ต้องรู้แค่ไหน |
|---|---|
| TypeScript | ใช้งานได้ทั่วไป รู้จัก type, interface, Generic |
| Hexagonal Architecture | เข้าใจแนวคิด Port & Adapter |
| Clean Architecture | เข้าใจ Dependency Rule |
| CQRS | รู้ว่า Command กับ Query แตกต่างกันอย่างไร |
| Domain-Driven Design | เข้าใจ Bounded Context และ Domain Logic |
| Nx Monorepo | เคยใช้งาน หรืออ่าน overview มาบ้าง |
ถ้าไม่รู้จักแนวคิดพวกนี้เลย ไม่ต้องกังวล — แต่ละบทความจะอธิบายในบริบทที่ใช้จริง ไม่ใช่แค่ทฤษฎี
References
| แนวคิด | แหล่งอ้างอิง |
|---|---|
| Hexagonal Architecture | Alistair Cockburn — Hexagonal Architecture |
| Clean Architecture | The Clean Architecture — Uncle Bob |
| CQRS | CQRS — Martin Fowler |
| Domain-Driven Design | Domain-Driven Design Reference — Eric Evans |
| Bounded Context | BoundedContext — Martin Fowler |
| Nx Monorepo | Nx Official Documentation |
ปัญหาที่ Architecture นี้แก้
ก่อนเข้าสู่ solution ลองดูว่าปัญหาที่ทุกคนน่าจะเคยเจอมีอะไรบ้าง
Circular dependency ที่แก้ไม่จบ เคยไหม — อยากแยก feature ใหม่ออกมา แต่ดัน import กันวนจนแตะโค้ดแล้วกระทบทั้งระบบ ไม่มีจุดที่ตัดได้
Business Logic กระจัดกระจาย ไม่รู้ว่า rule อยู่ที่ไหน validation บางส่วนอยู่ใน controller บางส่วนอยู่ใน service บางส่วนอยู่ใน helper ที่ไม่รู้ว่าใครเขียน คนใหม่ในทีมต้อง grep ทั้ง codebase เพื่อแก้ bug เดียว
Test ยากเพราะ side-effect ปนกับ business logic function ที่ควรเป็น pure กลับต้อง mock database, HTTP client, และ config พร้อมกัน เพื่อ test แค่การคำนวณตัวเลข
พอ traffic พุ่ง ต้องแยก service แต่ codebase ไม่พร้อม ทุกอย่างผูกกันหมด ต้องใช้เวลาหลายเดือนเพื่อ refactor ก่อนแยกได้ ซึ่งในช่วงนั้น feature ก็หยุดชะงัก
Architecture นี้ออกแบบมาเพื่อป้องกันทั้ง 4 ปัญหานี้ตั้งแต่วันแรก
หลักการออกแบบ
ระบบออกแบบเป็น Modular Monolith บน Monorepo โดยผสมแนวคิด 4 ตัวเข้าด้วยกัน
| แนวคิด | บทบาทในระบบ |
|---|---|
| Hexagonal Architecture | แยก Business Logic ออกจาก Infrastructure |
| Clean Architecture (Functional) | ให้ dependency ไหลทิศทางเดียว |
| CQRS | แยก Write กับ Read ในระดับ Use Case |
| Domain-Driven Design | แบ่ง Bounded Context ให้ชัดเจน |
Project Types และ Dependency Graph
ระบบแบ่งออกเป็น 5 project type ที่แต่ละชั้นมีความรับผิดชอบชัดเจน
@system/share-core ← Abstraction Layer
(Repository I/O Types + Interfaces + Provider Contracts)
@system/share-data ← Data Layer
(Repository Implementations)
@system/share-client ← HTTP ServiceClient Implementations
@system/share-service ← Service Layer
(Domain Logic + UseCase + Internal ServiceClient)
@system/app-{name} ← Http/Consumer Layer
(Framework + Composition Root)
กฎเด็ดขาด — dependency ไหลขึ้นข้างบนเท่านั้น ห้าม depend กลับทาง

กฎนี้ไม่ใช่แค่ convention — exports field ใน package.json บังคับ encapsulation จริง ๆ ถ้า path ไม่ได้ประกาศไว้ import ไม่ได้เลยทั้ง runtime และ TypeScript
การแบ่ง layer ตามนี้ยังกำหนด Test Strategy ของแต่ละ layer ด้วย แต่ละ layer mock สิ่งที่ต่างกัน และ test ในระดับที่ต่างกัน series นี้จะอธิบาย Testing Overview ใน Part 3 ก่อนลงลึกแต่ละ layer
ข้อดีหลักที่ได้จาก Architecture นี้
1. ประหยัดค่า Operate แต่ไม่ติดกับดัก Monolith
เริ่มต้น deploy ได้เหมือน Monolith ธรรมดา แต่เมื่อ Bounded Context ไหน traffic สูง แยกเป็น Microservice ได้โดยแก้โค้ดแค่ 2 จุด
// เดิม — Internal (same server)
const inventoryClient = new InventoryInternalClient(reserveInventoryUseCase)
// เปลี่ยนเป็น — HTTP (separate service)
const inventoryClient = new InventoryRemoteClient(env.INVENTORY_URL)
เปลี่ยนแค่บรรทัดนี้ ที่ app.ts — โค้ดส่วนอื่นไม่ต้องแตะ
2. Dependency ไหลทิศทางเดียว — Nx trace ได้ precise
การแยก project ตาม concern ทำให้ Nx สามารถ trace affected graph ได้แม่นยำ
แก้ share-service/{bc}-api
→ affected เฉพาะ app ที่ใช้ BC นั้น
แก้ share-core/{bc}-api
→ affected share-data, share-client, share-service, app ของ BC นั้น
แก้ share-client
→ ไม่ affected share-service (ไม่มี dependency กัน)
CI/CD build เฉพาะส่วนที่เปลี่ยน ไม่ต้อง rebuild ทั้งระบบ
3. Encapsulation จริง ด้วย exports field
share-service expose ระดับ action ทำให้ชัดเจนว่าอะไร public อะไร private
// ✅ ใช้ได้ — อยู่ใน exports field
import { makePlaceOrderEndpoint }
from '@system/share-service/order-api/command/place-order'
// ❌ ใช้ไม่ได้ — internals ถูกซ่อน แม้ไฟล์มีอยู่จริง
import { validatePolicy }
from '@system/share-service/order-api/command/place-order/logic/business.logic'
4. Business Logic แยกชัด ทดสอบง่าย
แบ่ง Business Logic เป็น 4 ประเภทที่มีกฎชัดเจน
| ประเภท | Pure เสมอ | ตัวอย่าง |
|---|---|---|
| Constraint | ✅ | ห้ามสั่งสินค้าจำนวน 0 |
| Calculation | ✅ | คำนวณราคารวมของ Order |
| Transform | ✅ | แปลง DTO → Domain Entity |
| Business Rule | ⚠️ แล้วแต่ | ตรวจว่า username ซ้ำไหม |
Constraint, Calculation, และ Transform เป็น pure function ทั้งหมด — test ได้โดยไม่ต้อง mock อะไรเลย
5. ServiceClient มี Source of Truth เดียว
Provider Contract define ที่ share-core ที่เดียว Consumer ใช้ Pick<> inline ไม่มีการ re-declare type ใหม่
type Deps = {
inventoryClient: Pick<InventoryServiceClient, 'reserve' | 'checkStock'>
// ↑ Pick inline — ไม่มี type แยก ไม่ซ้ำซ้อน
}
เมื่อ Provider เปลี่ยน TypeScript จับ error ให้อัตโนมัติทุกจุด
เหมาะกับทีมแบบไหน
✅ เหมาะ ถ้าทีมมีลักษณะนี้
ทีมที่กำลังสร้าง product ระยะยาว วางโครงสร้างดีตั้งแต่ต้น ดีกว่ามา refactor ใหญ่ทีหลังตอนทีมโตแล้ว ต้นทุนการเรียนรู้ตอนนี้ถูกกว่ามาก
ทีมที่รู้ว่าระบบจะโต ถ้า roadmap ชัดว่าจะมี traffic สูง หรือต้องขยายทีมในอนาคต Architecture นี้ลงทุนครั้งเดียวแล้วไม่ต้องสร้างใหม่
ทีมที่มี Senior อย่างน้อย 1-2 คน ไม่ต้องทั้งทีมเชี่ยวชาญ แต่ต้องมีคนที่ hold architectural decision ได้และช่วย onboard คนอื่น
⚠️ ต้องระวัง ถ้าเป็นแบบนี้
ทีมที่รีบ ship feature มาก Learning curve ช่วงแรกมีอยู่จริง โดยเฉพาะ Dependency Graph และ Export Strategy ถ้า deadline แน่นมาก อาจ friction ในช่วงแรก
ทีมที่ยังไม่คุ้น DDD / CQRS Bounded Context และการแยก Command/Query ต้องใช้เวลา internalize ถ้าทุกคนในทีมยังใหม่กับแนวคิดนี้ อาจเขียนโค้ดที่ “ผิดที่” โดยไม่รู้ตัว
❌ ไม่เหมาะ ถ้า
- เป็น side project หรือ prototype ที่ไม่แน่ใจว่าจะ production
- ทีม 1-2 คน และ scope เล็กมาก ไม่มี plan โต
- ไม่มี Senior ที่ช่วย enforce กฎใน codebase ได้
สรุป
Architecture นี้ไม่ใช่ Silver Bullet แต่ถ้าทีมกำลังสร้าง product จริงและมีแผนโต มันแก้ปัญหาที่เจ็บปวดที่สุด 4 ข้อพร้อมกัน ทั้ง circular dependency, business logic กระจัดกระจาย, test ยาก, และ scale ไม่ได้
ลงทุนครั้งเดียว ได้โครงสร้างที่โตไปพร้อมกับทีม
Article ถัดไปใน Series
| Part | หัวข้อ | สรุป |
|---|---|---|
| 2 | share-core — Abstraction Layer | Contract Design, Repository Interface, Provider Contract |
| 3 | Testing Overview | Test Matrix, Mock Strategy, Fixture vs Seed Data ทั้ง Series |
| 4 | share-data — Data Layer | Action-Based Structure, Entry, Task, DAF |
| 5 | share-data — Test Strategy | Fixtures, Mock Level, Jest config, SonarQube |
| 6 | share-client — HTTP ServiceClient | Remote Client, implements contract |
| 7 | share-service — Domain Logic | Constraint, Calculation, Transform, Rule |
| 8 | share-service — Orchestrator Pattern | CQRS, routeSteps, endpoint.config |
| 9 | app-{name} — Composition Root | Wiring, Monolith→Microservice, Integration Test |
FAQ
Q: Modular Monolith ต่างจาก Monolith ธรรมดาอย่างไร?
Monolith ธรรมดาไม่มีการแบ่ง boundary ที่ชัดเจน ทุกอย่างผูกกันจน refactor ยาก Modular Monolith แบ่ง codebase เป็น module ที่มี boundary ชัดเจนและ dependency ไหลทิศทางเดียว deploy เป็น process เดียวเหมือนกัน แต่โครงสร้างภายในพร้อมแยกได้ทุกเมื่อ
Q: ทำไมถึงเลือก Nx Monorepo แทนการแยก repository?
Nx ให้ประโยชน์หลัก 2 อย่างที่สำคัญกับ Architecture นี้ ได้แก่ affected graph ที่ trace ได้ precise ทำให้ CI/CD build เฉพาะส่วนที่เปลี่ยนจริง และการ enforce dependency rule ระหว่าง project ผ่าน exports field ที่บังคับ encapsulation จริง ไม่ใช่แค่ convention
Q: Architecture นี้เหมาะกับ project ที่เริ่มต้นใหม่เท่านั้นหรือเปล่า?
เหมาะกับ project ใหม่มากที่สุดเพราะวาง structure ได้ตั้งแต่ต้น แต่สามารถ migrate ทีละ Bounded Context ได้ เริ่มจาก BC ที่ใหม่หรือ BC ที่กำลัง refactor อยู่แล้ว ไม่จำเป็นต้อง migrate ทั้งระบบพร้อมกัน
Q: ต้องเข้าใจ DDD อย่างลึกซึ้งก่อนใช้ Architecture นี้ไหม?
ไม่จำเป็นต้องเชี่ยวชาญ DDD ทั้งหมด series นี้ใช้แค่แนวคิด Bounded Context เป็นหลักในการแบ่ง folder structure ซึ่งเป็นส่วนที่เข้าใจง่ายที่สุดของ DDD มีคนในทีมที่เข้าใจและ enforce ได้สัก 1-2 คนก็เพียงพอในช่วงเริ่มต้น
Q: ข้อผิดพลาดที่พบบ่อยที่สุดเมื่อเริ่มใช้ Architecture นี้คืออะไร?
ข้อผิดพลาดที่พบบ่อยที่สุดคือการ import ข้าม layer โดยตรง เช่น share-service import จาก share-data แทนที่จะรับ implementation ผ่าน Dependency Injection ที่ app.ts วิธีป้องกันคือตั้งค่า exports field ให้ครบตั้งแต่ต้น และ configure Nx boundary rules เพื่อให้ TypeScript และ Nx จับ error ให้อัตโนมัติ
Q: Architecture นี้เกี่ยวข้องกับ Clean Architecture ของ Uncle Bob อย่างไร?
ยืมหลักการ Dependency Rule มาใช้ โดย dependency ไหลเข้าหา layer ที่ abstract กว่าเสมอ แต่ปรับให้เป็น Functional Style แทน Class-based และแบ่ง layer ตาม concern จริงของระบบ แทนที่จะยึด 4 concentric circles ตามตัวอย่างดั้งเดิม ทำให้ได้ประโยชน์ของ Clean Architecture โดยไม่มี boilerplate ที่มากเกินจำเป็น