การสร้าง Banking API ด้วยการออกแบบที่มาก่อน: จากการกำหนดความต้องการ 5 โดเมนสู่การตรวจสอบ PoC และแผนงานสู่ Production
สิ่งที่คุณจะได้เรียนรู้
- วิธีจัดการข้อจำกัดที่ห้ามผิดพลาดในการออกแบบ 5 โดเมน: บัญชี สินเชื่อ อัตราแลกเปลี่ยน การลงทุน และ KYC
- วิธีบังคับขอบเขตโดเมนด้วย Modular Monolith (รวม ArchUnit)
- การใช้ BigDecimal, Pessimistic Locking, Transactional Outbox และ Idempotency Key เพื่อคุณภาพระดับการเงิน
- สิ่งที่ PoC เผยให้เห็นและเชื่อมต่อกับขั้นตอนถัดไปอย่างไร
กลุ่มเป้าหมาย
- นักพัฒนาที่สนใจการออกแบบและการใช้งาน API สำหรับระบบการเงิน/ฟินเทค
- ผู้ที่ต้องการฝึกฝน DDD / การออกแบบโดเมนโมเดล
- Backend engineer ที่ต้องการเข้าใจเหตุผลเบื้องหลังการออกแบบของ Spring Boot + Java
สภาพแวดล้อม
| รายการ | เวอร์ชัน |
|---|---|
| Java | 21 (Virtual Threads) |
| Spring Boot | 3.x |
| PostgreSQL | 16 |
| Redis | 7 |
| Build Tool | Maven (multi-module) |
| Container | Docker Compose |
บทนำ
เมื่อใช้งานระบบธนาคาร ความท้าทายแรกไม่ใช่ความกว้างของฟีเจอร์ แต่เป็นวิธีจัดการพื้นที่ที่ความล้มเหลวไม่เป็นที่ยอมรับ
- ห้ามมีข้อผิดพลาดในการคำนวณ
- ห้ามมีการประมวลผลซ้ำ
- ต้องมีความสามารถในการตรวจสอบ
- ต้องรองรับการแยกหรือขยายในอนาคต
บทความนี้อธิบายกระบวนการสร้าง Banking API ใน 5 โดเมน ได้แก่ บัญชี สินเชื่อ อัตราแลกเปลี่ยน การลงทุน และ KYC โดยมีโครงสร้างตาม 3 ขั้นตอนการออกแบบ
แกนหลักคือ:
- การกำหนดความต้องการ
- การออกแบบขั้นพื้นฐาน
- การออกแบบรายละเอียด
จากนั้นเราทดสอบการออกแบบนั้นเป็น PoC เพื่อดูว่าอะไรสามารถตรวจสอบได้ และความท้าทายของขั้นตอนถัดไปอยู่ที่ใด — นำเสนอเป็นแผนงาน
เป้าหมายไม่ใช่บทความ “ดูสิ่งที่ฉันสร้าง” แต่เป็นคำอธิบายทางเทคนิคที่มีพื้นฐานจากเหตุผลในการออกแบบ
📌 การกำหนดความต้องการ: ข้อจำกัดที่กำหนดไว้ล่วงหน้าใน 5 โดเมน
นโยบายโดยรวม
สิ่งแรกที่ตัดสินใจในการกำหนดความต้องการไม่ใช่รายการฟีเจอร์ แต่เป็นข้อจำกัดที่ห้ามละเมิด
- การคำนวณทางการเงินต้องแม่นยำและสม่ำเสมอ
- การเปลี่ยนสถานะที่ไม่ถูกต้องต้องถูกปฏิเสธในระดับโดเมน
- การดำเนินการที่สำคัญต้องตรวจสอบได้
- การรวมแบบ Async ต้องไม่ทำลายความสอดคล้อง
นโยบายเหล่านี้ถูกใช้ใน 5 โดเมน
1. โดเมนการจัดการบัญชี
ผู้ใช้งาน
| ผู้ใช้งาน | คำอธิบาย |
|---|---|
| เจ้าของบัญชี | ผู้ถือบัญชี ดำเนินการตรวจสอบยอดคงเหลือ ฝาก/ถอน และโอน |
| พนักงานธนาคาร | ผู้ดำเนินการจัดการการเปิดบัญชี ปิดบัญชี และการเปลี่ยนสถานะ |
| Batch หักเงินอัตโนมัติ | ระบบประมวลผลการหักเงินตามกำหนด |
ข้อจำกัดการดำเนินการตามสถานะบัญชี
บัญชีมี 4 สถานะ แต่ละสถานะมีการดำเนินการที่อนุญาตแตกต่างกัน
| สถานะ | ฝาก | ถอน | โอน | หมายเหตุ |
|---|---|---|---|---|
| ACTIVE | ✅ | ✅ | ✅ | การใช้งานปกติ |
| FROZEN | ❌ | ❌ | ❌ | ตรวจพบการฉ้อโกง การตรวจสอบตัวตนอยู่ระหว่างดำเนินการ ฯลฯ |
| DORMANT | ✅ | ❌ | ❌ | ไม่มีการทำธุรกรรมเป็นระยะเวลาหนึ่ง |
| CLOSED | ❌ | ❌ | ❌ | เปลี่ยนสถานะหลังยืนยันยอดคงเหลือเป็นศูนย์ |
การเปลี่ยนสถานะ
[เปิดบัญชี]
│
▼
ACTIVE ◄──────────────────────────────────┐
│ │
┌────────┼────────────────┐ │
│ │ │ [ตรวจสอบตัวตนสำเร็จ]
[AML ตรวจ [ไม่มีกิจกรรม [ยอดเป็นศูนย์ + (เฉพาะ FROZEN_MANUAL)
พบ/ระงับ] 2 ปี] ขอปิดบัญชี]
│ │
│ CLOSED
▼
DORMANT ──────────────────────────────►ACTIVE
(ฝากอย่างเดียว) [คำขอสาขา + ยืนยันตัวตน]
│
▼
FROZEN ──────────────────────────────────────────►ACTIVE
(ไม่มีการดำเนินการ) [FROZEN_MANUAL: ผู้ดำเนินการยกเลิกระงับ]
[FROZEN_AML: เฉพาะเจ้าหน้าที่ compliance]
FROZEN มี 2 ประเภทย่อยที่มีสิทธิ์การยกเลิกระงับต่างกัน
| ประเภทการระงับ | ทริกเกอร์ | สิทธิ์การยกเลิกระงับ |
|---|---|---|
FROZEN_MANUAL | ระงับด้วยตนเองโดยผู้ดำเนินการสาขา | ผู้ดำเนินการ (หลังยืนยันตัวตน) |
FROZEN_AML | ตรวจพบอัตโนมัติโดยระบบ AML | เฉพาะเจ้าหน้าที่ compliance |
📝 Note: ความต้องการหลักที่นี่คือวิธีปกป้องค่าที่สอดคล้องอย่างเข้มข้นเช่นยอดคงเหลือ และการดำเนินการสามารถหยุดทันทีเมื่อตรวจพบการฉ้อโกงได้หรือไม่
2. โดเมนสินเชื่อ
ผู้ใช้งาน
| ผู้ใช้งาน | คำอธิบาย |
|---|---|
| ผู้สมัครสินเชื่อ | ลูกค้าที่สมัครสินเชื่อ ทำการจำลอง และชำระคืน |
| ฝ่ายตรวจสอบ | ผู้ดำเนินการตรวจสอบใบสมัครและตัดสินใจอนุมัติ/ปฏิเสธ |
| ระบบจัดการหนี้ | ระบบภายนอกที่ตรวจสอบความล่าช้าและคำนวณค่าปรับ |
การจัดการสถานะใบสมัคร
APPLIED
→ PRE_SCREENING
→ FULL_SCREENING
→ CONTRACTED ─── ไม่ดำเนินการภายใน 30 วัน ───▶ EXPIRED
→ EXECUTED
→ REJECTED ← สามารถเกิดขึ้นได้ทั้งในขั้นตอน pre- หรือ full-screening
→ WITHDRAWN ← ผู้สมัครสามารถถอนตั้งแต่ APPLIED ถึง FULL_SCREENING
- ไม่อนุญาตให้ย้อนสถานะ
- ห้ามสมัครใหม่หลังถูกปฏิเสธเป็นเวลา 60 วัน (ระยะระงับ)
- ระยะเวลาความถูกต้อง 30 วันจาก
CONTRACTEDถึงEXECUTED; batch เปลี่ยนอัตโนมัติเป็นEXPIREDหากเกินกำหนด
การคำนวณการชำระเงินเท่ากัน (Level Payment)
การชำระเงินรายเดือน $M$ คำนวณดังนี้:
$$M = P \frac{r(1+r)^n}{(1+r)^n - 1}$$
- $P$: จำนวนเงินต้น
- $r$: อัตรารายเดือน (อัตรารายปี ÷ 12)
- $n$: จำนวนงวด (เดือน)
การคำนวณทั้งหมดใช้ BigDecimal ที่มีสเกล 30 หลักสำหรับการคำนวณกลาง งวดสุดท้ายรับผิดชอบข้อผิดพลาดการปัดเศษสะสม
การคำนวณค่าปรับความล่าช้า
$$\text{ค่าปรับ} = \text{ยอดเงินต้นคงเหลือ} \times \text{อัตราค่าปรับ} \times \frac{\text{วันที่เกินกำหนด}}{365}$$
วิธีนับวันก็ถูกกำหนดไว้อย่างชัดเจน
| รายการ | ข้อกำหนด |
|---|---|
| วิธีนับวัน | ไม่รวมวันเริ่มต้น (ไม่รวมวันเริ่ม รวมวันสิ้นสุด) |
| วันในปี | คงที่ 365 (ปีอธิกสุรทินก็หารด้วย 365) |
| พื้นฐาน | แนวปฏิบัติธนาคารทั่วไป สอดคล้องกับการคำนวณค่าปรับตาม Money Lending Business Act |
⚠️ Warning: วิธีรวมหรือไม่รวมวันเริ่มต้นสร้างความแตกต่าง 1 วันในการคิดค่าปรับ
DayCountConventionถูกใช้งานเป็น enum เพื่อให้สลับได้เมื่อความต้องการผลิตภัณฑ์เปลี่ยนแปลง
3. โดเมนอัตราแลกเปลี่ยน
ผู้ใช้งาน
| ผู้ใช้งาน | คำอธิบาย |
|---|---|
| เจ้าของบัญชี | ลูกค้าที่ซื้อ/ขายสกุลเงินต่างประเทศและทำการจอง |
| Forex Dealer | พนักงานภายในที่จัดการการตั้งค่าอัตราและการกำหนด spread |
| ผู้ให้บริการข้อมูลตลาดภายนอก | บริการภายนอกที่ให้อัตราแลกเปลี่ยนแบบเรียลไทม์ |
การใช้ Spread และทิศทางการคำนวณ
การคำนวณ Forex ใช้การคูณหรือหารขึ้นอยู่กับข้อตกลงคู่สกุลเงิน (Base/Quote Currency)
| ทิศทาง | ตัวอย่าง (USD/JPY=152) | อัตราที่ใช้ | สูตร |
|---|---|---|---|
| ซื้อ FX (JPY → USD) | 1 USD = 152.50 JPY (Ask) | MidRate + Spread | SourceAmount ÷ AppliedRate |
| ขาย FX (USD → JPY) | 1 USD = 151.50 JPY (Bid) | MidRate - Spread | SourceAmount × AppliedRate |
🚨 Alert: การสลับทิศทางการคำนวณจะทำให้กำไรและขาดทุนกลับด้าน
CurrencyPairถูกกำหนดเป็น enum ที่รวมการคูณ/หารเพื่อป้องกันข้อผิดพลาด
การปัดเศษตามสกุลเงิน (มุมมองกำไรธนาคาร)
| สกุลเงิน | การปัดเศษ BUY | การปัดเศษ SELL |
|---|---|---|
| JPY | CEILING (ปัดขึ้น) | DOWN (ตัดทิ้ง) |
| USD / EUR / GBP | CEILING | DOWN |
เมื่อลูกค้าซื้อสกุลเงินต่างประเทศ การปัดขึ้นหมายถึงลูกค้าจ่ายเล็กน้อยมากขึ้น เมื่อขาย การตัดทิ้งหมายถึงลูกค้าได้รับเล็กน้อยน้อยลง — สะสมกำไรเล็กน้อยสำหรับธนาคาร
Volatility Circuit Breaker (ระงับอัตโนมัติ)
หากอัตราคู่สกุลเงินเปลี่ยนแปลง 3% หรือมากกว่าใน 1 นาที การซื้อขายใหม่สำหรับคู่นั้นจะถูกระงับโดยอัตโนมัติ ระยะเวลาระงับเริ่มต้นคือ 5 นาที กำหนดค่าได้ผ่านตาราง volatility_circuit_breaker_settings Forex dealer สามารถรีสตาร์ทด้วยตนเองจากหน้าจอผู้ดูแล
4. โดเมนการลงทุน
ผู้ใช้งาน
| ผู้ใช้งาน | คำอธิบาย |
|---|---|
| นักลงทุน | ลูกค้าที่ซื้อ/ขาย Investment Trust และดูพอร์ตโฟลิโอ |
| ธนาคารทรัสต์ | สถาบันภายนอกที่จัดการการดำเนินการ การตั้งค่าวันที่ชำระเงิน และการอัปเดต NAV |
| หน่วยงานภาษี | การประมวลผลภาษีสำหรับการใช้ช่อง NISA และการคำนวณกำไร/ขาดทุนบัญชีเฉพาะ |
หลักการ Blind Pricing (Forward Pricing)
NAV ของ Investment Trust ไม่ถูกกำหนดในเวลาสั่งซื้อ การออกแบบคำนึงถึง blind pricing นี้
- คำสั่งรับเป็นการระบุจำนวนเงิน (BUY) หรือการระบุหน่วย/การขายทั้งหมด (SELL)
- “กี่หน่วย” คำนวณโดย batch หลังจาก NAV วันทำการถัดไปถูกยืนยัน เขียนกลับไปยัง
investment_orders.units - UI แสดง “หน่วยโดยประมาณ” พร้อมบ่งบอกชัดเจนว่ารอการยืนยัน
สถานะคำสั่ง: PENDING (รับแล้ว) → ACCEPTED (สำรองเงินแล้ว) → EXECUTED (ยืนยันหน่วยแล้ว)
การจัดการช่อง NISA ที่ยกเว้นภาษี
จัดการเพดานรายปีและตลอดชีพสำหรับ NISA ใหม่ที่เริ่มใช้ในปี 2024
- ช่องการลงทุนสะสม: 1.2 ล้านเยน/ปี
- ช่องการลงทุนเพื่อการเติบโต: 2.4 ล้านเยน/ปี
- ช่องการลงทุนตลอดชีพรวม: 18 ล้านเยน
การออกแบบยังรวม “batch คืนค่าปีถัดไป” ที่คืนช่องตลอดชีพในวันที่ 1 มกราคมของปีหลังจากการไถ่ถอน
ต้นทุนการได้มาเฉลี่ย (Moving Average Method)
ต้นทุนรวมการได้มาอัปเดตในแต่ละการซื้อเพิ่มเติม การขายบางส่วนหักออก “หน่วยที่ขาย × ต้นทุนการได้มาเฉลี่ยก่อนการขาย” ตรรกะนี้ถูกรวมไว้ในเมธอดโดเมน Holding.addUnits() / Holding.removeUnits() ป้องกันการแก้ไขโดยตรงจากภายนอก
5. โดเมน Auth / KYC
ผู้ใช้งาน
| ผู้ใช้งาน | คำอธิบาย |
|---|---|
| ลูกค้า | ผู้ใช้ที่ผ่านการตรวจสอบตัวตน eKYC |
| ผู้ตรวจสอบ KYC | พนักงานภายในที่ตรวจสอบเอกสาร อนุมัติ และปฏิเสธ |
| ระบบ | Batch ประมวลผลการตรวจสอบวันหมดอายุเอกสารและการอัปเดตสถานะอัตโนมัติ |
การเปลี่ยนสถานะ eKYC
UNVERIFIED (เริ่มต้น)
→ DOCUMENTS_SUBMITTED
→ UNDER_REVIEW
→ VERIFIED
→ REJECTED
→ RESUBMISSION_REQUIRED
→ DOCUMENTS_SUBMITTED (ส่งใหม่)
การควบคุมการเข้าถึง API ตามสถานะ
| สถานะ KYC | ฟีเจอร์ที่ใช้งานได้ |
|---|---|
VERIFIED | ทุกฟีเจอร์ (โอน อัตราแลกเปลี่ยน การลงทุน) |
UNDER_REVIEW | เฉพาะการตรวจสอบยอดคงเหลือและประวัติ |
| อื่นๆ | เฉพาะการตรวจสอบขั้นพื้นฐาน |
การออกแบบ Auth/การตรวจสอบสิทธิ์มุ่งเน้นที่ว่าจะวางขอบเขตที่ใด ไม่ใช่ความสะดวก การไหลการตรวจสอบสิทธิ์ (PAR / JARM / MTLS) มอบหมายให้ IdP ภายนอก (Keycloak / Auth0 ฯลฯ) ในขณะที่ API server มุ่งเน้นเฉพาะการตรวจสอบ token
🏗️ การออกแบบขั้นพื้นฐาน: บังคับขอบเขตด้วย Modular Monolith
ทำไมถึงเป็น Modular Monolith
เป้าหมายในขั้นตอนนี้คือรักษาความเร็วในการพัฒนาในกระบวนการเดียวขณะรักษาความสามารถในการแยกในอนาคต
โครงสร้างที่นำมาใช้คือการแยกโมดูลตามโดเมน:
- common
- auth
- accounts
- loans
- forex
- investments
- app
สิ่งนี้อนุญาตให้ดำเนินการเป็นกระบวนการเดียวในตอนแรกขณะทำขอบเขตการพึ่งพาชัดเจน
การออกแบบ API Endpoint ตามโดเมน
API การจัดการบัญชี
| Method | Path | คำอธิบาย |
|---|---|---|
GET | /v1/accounts/{accountId}/balance | ดึงยอดคงเหลือแบบเรียลไทม์ |
GET | /v1/accounts/{accountId}/transactions | ประวัติการทำธุรกรรม (cursor pagination) |
POST | /v1/accounts/{accountId}/withdraw | ถอนเงิน ⚠️ ต้องใช้ X-Idempotency-Key |
POST | /v1/accounts/{accountId}/deposit | ฝากเงิน ⚠️ ต้องใช้ X-Idempotency-Key |
POST | /v1/accounts/{accountId}/transfer | โอนเงิน ⚠️ ต้องใช้ X-Idempotency-Key |
GET | /v1/accounts/{accountId} | ดึงข้อมูลบัญชี |
Endpoint POST ทั้งหมดต้องใช้ header X-Idempotency-Key การส่งซ้ำด้วย key เดิมจะคืนผลลัพธ์เดิม (การรับประกัน idempotency)
ประวัติการทำธุรกรรมใช้ cursor pagination
GET /v1/accounts/{accountId}/transactions?limit=20&cursor=txn_20260502_000099&from=2026-04-01&to=2026-05-01
{
"transactions": [...],
"nextCursor": "txn_20260430_000078",
"hasMore": true
}
API สินเชื่อ
| Method | Path | คำอธิบาย |
|---|---|---|
POST | /v1/loans/simulations | จำลองการชำระคืน (ตารางชำระ) |
POST | /v1/loans/applications | สมัครสินเชื่อ |
GET | /v1/loans/applications/{applicationId} | ตรวจสอบสถานะใบสมัคร |
GET | /v1/loans/contracts/{contractId} | ดึงข้อมูลสัญญา |
GET | /v1/loans/contracts/{contractId}/schedule | ดึงตารางการชำระคืน |
POST | /v1/loans/contracts/{contractId}/repayments | ดำเนินการชำระคืนตามกำหนด (monthly batch) |
POST | /v1/loans/contracts/{contractId}/prepayments | ชำระคืนก่อนกำหนด |
API จำลองส่งคืนตารางชำระในผลลัพธ์
// Request
POST /v1/loans/simulations
{
"principal": 3000000,
"annualInterestRate": 0.035,
"termMonths": 60
}
// Response
{
"monthlyPayment": 54687,
"totalPayment": 3281220,
"totalInterest": 281220,
"schedule": [
{ "installmentNo": 1, "paymentDate": "2026-06-27",
"principal": 46187, "interest": 8500, "balance": 2953813 }
]
}
การชำระคืนก่อนกำหนดระบุ prepaymentType (ลดระยะเวลา / ลดยอดชำระ) และตารางการชำระคืนทั้งหมดจะถูกคำนวณใหม่หลังดำเนินการ
API อัตราแลกเปลี่ยน
| Method | Path | คำอธิบาย |
|---|---|---|
GET | /v1/forex/rates | รายการอัตราปัจจุบันสำหรับคู่สกุลเงิน |
GET | /v1/forex/quotes | ดึงการเสนอราคาการซื้อขาย (ออก QuoteID) |
POST | /v1/forex/exchange | ดำเนินการแลกเปลี่ยนโดยใช้ QuoteID |
GET | /v1/forex/history | ดึงประวัติธุรกรรม Forex |
แนวทาง 2 ขั้นตอนแยกการดึงใบเสนอราคาและการดำเนินการถูกนำมาใช้
GET /v1/forex/quotes?sourceCurrency=JPY&targetCurrency=USD&sourceAmount=100000
{
"quoteId": "QT-20260502-00001",
"sourceCurrency": "JPY", "targetCurrency": "USD",
"sourceAmount": 100000, "targetAmount": 657.89,
"rate": 152.00, "spread": 0.50, "appliedRate": 152.50,
"expiresAt": "2026-05-02T12:00:30Z"
}
QuoteID มี TTL 30 วินาทีที่จัดการใน Redis ใช้ได้ครั้งเดียวเท่านั้น ใบเสนอราคาที่หมดอายุหรือใช้ซ้ำจะถูกปฏิเสธ
API การลงทุน
| Method | Path | คำอธิบาย |
|---|---|---|
GET | /v1/investments/portfolio | ดึงสถานะพอร์ตโฟลิโอและกำไร/ขาดทุน |
GET | /v1/investments/funds | รายการ Investment Trust ที่ใช้งานได้ |
GET | /v1/investments/funds/{fundCode} | รายละเอียด Investment Trust และประวัติ NAV |
POST | /v1/investments/orders | คำสั่งซื้อ/ขาย |
DELETE | /v1/investments/orders/{orderId} | ยกเลิกคำสั่ง (เฉพาะก่อน cutoff และขณะ PENDING) |
GET | /v1/investments/orders/{orderId} | ตรวจสอบสถานะคำสั่ง |
GET | /v1/investments/tax-exemptions | ยืนยันการใช้ช่อง NISA |
ไม่อนุญาตให้ยกเลิกคำสั่งหลังจาก cutoff time (15:00 JST) หรือเมื่อสถานะเป็น ACCEPTED หรือหลังจากนั้น
API Auth / KYC
| Method | Path | คำอธิบาย |
|---|---|---|
GET | /api/v1/kyc/me | สอบถามสถานะ KYC ของตัวเอง |
POST | /api/v1/kyc/submit | ส่งเอกสาร (การเปลี่ยนสถานะ) |
GET | /api/v1/kyc/{customerId} | ผู้ตรวจสอบ: สอบถาม KYC ลูกค้า |
PUT | /api/v1/kyc/{customerId}/review | ผู้ตรวจสอบ: อัปเดตผลการตรวจสอบ |
การออกแบบโมเดลข้อมูล: ER Diagram ตามโดเมน
การจัดการบัญชี
┌─────────────────────────────┐
│ accounts │
├─────────────────────────────┤
│ account_id VARCHAR PK │
│ customer_id VARCHAR FK │
│ account_type VARCHAR │ (ORDINARY / SAVINGS / CURRENT)
│ status VARCHAR │ (ACTIVE / FROZEN / DORMANT / CLOSED)
│ balance DECIMAL │
│ currency CHAR(3) │
│ last_transaction_at TIMESTAMPTZ │
└─────────────────────────────┘
┌─────────────────────────────┐
│ transactions │ ※ append-only (ไม่มี UPDATE/DELETE)
├─────────────────────────────┤
│ transaction_id VARCHAR PK │
│ account_id VARCHAR FK │
│ transaction_type VARCHAR │ (DEPOSIT / WITHDRAWAL / TRANSFER_IN / TRANSFER_OUT)
│ amount DECIMAL │
│ balance_after DECIMAL │ (snapshot ยอดคงเหลือหลังธุรกรรม)
│ transfer_id VARCHAR │ (สำหรับการโอน: เชื่อมทั้งสองรายการด้วย ID เดียวกัน)
│ executed_at TIMESTAMPTZ│
└─────────────────────────────┘
┌─────────────────────────────┐
│ idempotency_keys │
├─────────────────────────────┤
│ idempotency_key VARCHAR PK │
│ response_body JSONB │
│ status_code INTEGER │
│ expires_at TIMESTAMPTZ│ (created_at + 24h)
└─────────────────────────────┘
📝 Note:
transactionsเป็น append-only การจัดเก็บยอดคงเหลือหลังธุรกรรมเป็น snapshotbalance_afterช่วยให้สอบถามยอดคงเหลือ ณ จุดใดก็ได้โดยไม่ต้องคำนวณใหม่
สินเชื่อ
┌─────────────────────────────┐
│ loan_applications │
├─────────────────────────────┤
│ application_id VARCHAR PK │
│ customer_id VARCHAR FK │
│ principal DECIMAL │
│ annual_rate DECIMAL │
│ term_months INTEGER │
│ status VARCHAR │ (APPLIED/.../EXECUTED/REJECTED/EXPIRED)
└─────────────────────────────┘
┌─────────────────────────────┐
│ loan_contracts │
├─────────────────────────────┤
│ contract_id VARCHAR PK │
│ outstanding DECIMAL │ (ยอดคงเหลือ)
│ accrued_late_charge DECIMAL │ (ค่าปรับที่ยังไม่ได้ชำระ อัปเดตโดย daily batch)
│ contract_expires_at TIMESTAMPTZ │ (ระยะเวลาความถูกต้อง CONTRACTED → EXECUTED)
└─────────────────────────────┘
┌─────────────────────────────┐
│ repayment_schedules │
├─────────────────────────────┤
│ contract_id VARCHAR FK │
│ installment_no INTEGER │
│ due_date DATE │
│ principal DECIMAL │
│ interest DECIMAL │
│ balance_after DECIMAL │
│ status VARCHAR │ (PENDING/PAID/OVERDUE)
│ schedule_version INTEGER │ (+1 ทุกครั้งที่คำนวณใหม่จากการชำระก่อนกำหนด)
└─────────────────────────────┘
ตารางชำระหลังชำระก่อนกำหนดจัดการเวอร์ชันโดยเพิ่ม schedule_version +1 เวอร์ชันเก่าถูกเก็บไว้เพื่อวัตถุประสงค์การตรวจสอบโดยไม่ลบ
อัตราแลกเปลี่ยน
┌─────────────────────────────┐
│ quote_records │ (การจัดการ QuoteStatus แบบถาวร)
├─────────────────────────────┤
│ quote_id VARCHAR PK │
│ currency_pair VARCHAR │
│ applied_rate DECIMAL │
│ status VARCHAR │ (PENDING / USED / EXPIRED)
│ expires_at TIMESTAMPTZ │
└─────────────────────────────┘
┌─────────────────────────────┐
│ forex_transactions │
├─────────────────────────────┤
│ source_currency CHAR(3) │
│ target_currency CHAR(3) │
│ source_amount DECIMAL │
│ target_amount DECIMAL │
│ direction VARCHAR │ (BUY / SELL)
│ quote_id VARCHAR FK │
└─────────────────────────────┘
┌─────────────────────────────┐
│ spread_settings │
├─────────────────────────────┤
│ currency_pair VARCHAR │
│ min_amount DECIMAL │
│ max_amount DECIMAL │
│ spread DECIMAL │
└─────────────────────────────┘
การลงทุน
┌─────────────────────────────┐
│ investment_orders │
├─────────────────────────────┤
│ order_type VARCHAR │ (BUY / SELL)
│ amount DECIMAL │ (การระบุจำนวนเงิน สำหรับ BUY)
│ units DECIMAL │ (ยืนยันหลังดำเนินการ ระบุตอนสั่งสำหรับ SELL)
│ estimated_units DECIMAL │ (หน่วยโดยประมาณสำหรับ blind pricing แสดง UI)
│ nav_at_order DECIMAL │ (NAV ณ เวลาดำเนินการ)
│ status VARCHAR │ (PENDING/ACCEPTED/EXECUTED/CANCELLED)
│ cancellable_until TIMESTAMPTZ │ (กำหนดยกเลิก: วันเดียวกัน 15:00 JST)
└─────────────────────────────┘
┌─────────────────────────────┐
│ holdings │
├─────────────────────────────┤
│ units DECIMAL │
│ acquisition_cost DECIMAL │ (ต้นทุนรวมการได้มา อัปเดตด้วย moving average)
└─────────────────────────────┘
┌─────────────────────────────┐
│ tax_exempt_slots │
├─────────────────────────────┤
│ slot_type VARCHAR │ (NISA_ACCUMULATE/NISA_GROWTH/LIFETIME)
│ year INTEGER │
│ used_amount DECIMAL │
│ limit_amount DECIMAL │
└─────────────────────────────┘
ทั่วไป (ตรวจสอบ / KYC)
┌─────────────────────────────┐
│ audit_logs │
├─────────────────────────────┤
│ trace_id VARCHAR │
│ operator_id VARCHAR │
│ operation_type VARCHAR │ (INSERT/UPDATE/DELETE/API_CALL)
│ resource_type VARCHAR │ (ชื่อตาราง หรือ API path)
│ before_value JSONB │ (ค่าก่อนเปลี่ยน)
│ after_value JSONB │ (ค่าหลังเปลี่ยน)
│ occurred_at TIMESTAMPTZ │
└─────────────────────────────┘
┌─────────────────────────────┐
│ kyc_verifications │
├─────────────────────────────┤
│ kyc_id VARCHAR PK │
│ customer_id VARCHAR FK │
│ status VARCHAR │ (KycStatus enum)
│ document_type VARCHAR │ (PASSPORT / DRIVING_LICENSE / MY_NUMBER)
│ verified_at TIMESTAMPTZ │
│ expires_at TIMESTAMPTZ │
│ rejected_reason VARCHAR │
└─────────────────────────────┘
บังคับกฎการออกแบบใน CI ไม่ใช่ Runtime ด้วย ArchUnit
ข้อตกลงด้วยคำพูดจะถูกละเมิด ดังนั้นสิ่งต่อไปนี้จึงถูกทำให้ตรวจสอบอัตโนมัติ:
- domain ต้องไม่พึ่งพา application
- domain ต้องไม่พึ่งพา api
- domain ต้องไม่พึ่งพา infrastructure
- application ต้องไม่พึ่งพา Controller
- Controller วางไว้ใต้ api package
- Service วางไว้ใต้ application package
- Repository วางไว้ใต้ domain.repository package
สิ่งนี้แก้ไขความคลาดเคลื่อนในการใช้งานผ่านการทดสอบ
เจตนาในการออกแบบโครงสร้างพื้นฐานทั่วไป
Transactional Outbox
อันตรายที่สุดในการรวมแบบ async คือความไม่ตรงกันระหว่างความสำเร็จของการอัปเดต DB และความล้มเหลวในการเผยแพร่ event
ดังนั้นการอัปเดตโดเมนและการเขียน outbox จึงถูกรวมไว้ในธุรกรรมเดียวกัน โดยมีกระบวนการแยกต่างหากจัดการการส่ง
- การเขียนสำเร็จแบบ atomic
- การส่งสามารถลองใหม่ได้
โครงสร้างนี้ทำหน้าที่เป็นจุดเชื่อมต่อสำหรับการย้ายไปยัง Kafka หรือระบบที่คล้ายกันในอนาคต
การตรวจสอบและการติดตาม
การแยกทางกายภาพและการประมวลผลแบบ async ของ audit log ก็เป็นส่วนหนึ่งของการออกแบบ
แต่ละ Service
│ การเผยแพร่ ApplicationEvent (ไม่ส่งผลต่อการประมวลผล sync)
▼
AuditEventPublisher (async)
│ ส่งไปยัง Kafka / Amazon Kinesis
▼
ผู้บริโภค Audit Log
├──▶ audit_logs DB (PostgreSQL instance แยก)
└──▶ DWH / S3 (เก็บระยะยาว / วิเคราะห์)
- การติดตามระดับคำขอผ่าน TraceId
- Audit log บันทึก ID ผู้ดำเนินการ เวลาทำรายการ ตารางเป้าหมาย และค่าก่อน/หลัง (JSON)
- ห้ามส่งออก PII ไปยัง log (เฉพาะ UUID
customerIdข้อมูลส่วนบุคคลดึงจาก DB)
💻 การออกแบบรายละเอียดและการใช้งาน: คุณภาพการเงินในโค้ด
Value Object: Money
Money ถูกกำหนดเป็น record ที่ไม่เปลี่ยนแปลงเป็นรากฐานสำหรับการคำนวณทางการเงิน
public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("จำนวนเงินต้องมากกว่าหรือเท่ากับศูนย์");
}
}
public Money add(Money other) {
assertSameCurrency(other);
return new Money(this.amount.add(other.amount), this.currency);
}
public Money subtract(Money other) {
assertSameCurrency(other);
if (this.amount.compareTo(other.amount) < 0) {
throw new IllegalArgumentException("ผลลบจะเป็นค่าลบ");
}
return new Money(this.amount.subtract(other.amount), this.currency);
}
/**
* JPY ใช้ HALF_DOWN (ตัดทิ้ง) อื่นๆ ใช้ HALF_UP
*/
public Money multiply(BigDecimal factor) {
RoundingMode mode = "JPY".equals(currency.getCurrencyCode())
? RoundingMode.DOWN : RoundingMode.HALF_UP;
int scale = "JPY".equals(currency.getCurrencyCode()) ? 0 : 2;
return new Money(
this.amount.multiply(factor).setScale(scale, mode), this.currency);
}
}
การดำเนินการ BigDecimal และ Pessimistic Locking
การอัปเดตยอดคงเหลือใช้ pessimistic locking (SELECT FOR UPDATE)
Optimistic locking ก็เป็นไปได้ แต่การอนุญาตให้ลองใหม่กับค่าที่สำคัญทางการเงินเช่นยอดคงเหลือจะเพิ่มความซับซ้อนของโค้ดและความเสี่ยงจากผลข้างเคียงของการลองใหม่ที่ไม่คาดคิด Pessimistic locking รับประกัน 1 คำขอ = 1 commit
ลำดับ withdrawal flow:
Client
│
▼ AccountWithdrawService.withdraw(accountId, amount, idempotencyKey)
│
├─ [1] idempotencyKeyRepository.find(idempotencyKey)
│ └─ หากมี: คืน response ที่บันทึกไว้ตามเดิม
│
├─ [2] เริ่ม @Transactional
│
├─ [3] accountRepository.findByIdForUpdate(accountId)
│ └─ SELECT ... FOR UPDATE (ได้รับ row lock)
│
├─ [4] dailyCacheRepository.findForUpdate(accountId, today)
│ └─ ดึงและล็อคยอดถอนสะสมวันนี้
│
├─ [5] account.withdraw(amount, todayAccumulated, limitPolicy)
│ └─ ตรวจสอบตามลำดับ: สถานะ → วงเงิน → ยอดคงเหลือ
│
├─ [6] transactionRepository.save(newTransaction)
│ └─ INSERT (balance_after = ยอดคงเหลือที่อัปเดตแล้ว)
│
├─ [7] dailyCacheRepository.update(+amount)
│
├─ [8] accountRepository.save(account)
│
├─ [9] idempotencyKeyRepository.save(idempotencyKey, response)
│
└─ [10] @Transactional commit
การคำนวณสินเชื่อ: Value Object: InterestRate
public record InterestRate(BigDecimal annualRate) {
/** คืนอัตรารายเดือน การคำนวณกลางใช้สเกล 30 หลัก */
public BigDecimal monthlyRate() {
return annualRate.divide(BigDecimal.valueOf(12), 30, RoundingMode.HALF_UP);
}
/** อัตรารายวัน (ใช้สำหรับดอกเบี้ยบางส่วนงวดแรกและการคำนวณค่าปรับ) */
public BigDecimal dailyRate(int daysInYear) {
return annualRate.divide(BigDecimal.valueOf(daysInYear), 30, RoundingMode.HALF_UP);
}
}
การคำนวณ level payment คำนวณดอกเบี้ยบางส่วนงวดแรก (ดอกเบี้ยรายวันจากวันเบิกจ่ายถึงวันชำระงวดแรก) แยกต่างหาก
// M = P * r * (1+r)^n / ((1+r)^n - 1) → สเกล 30 หลักสำหรับกลาง ปัดเป็นหน่วยตอนท้าย
BigDecimal r = rate.monthlyRate();
BigDecimal pow = rPlusOne.pow(termMonths).setScale(30, RoundingMode.HALF_UP);
BigDecimal monthlyPayment = principal.amount()
.multiply(r).multiply(pow)
.divide(pow.subtract(BigDecimal.ONE), 30, RoundingMode.HALF_UP)
.setScale(0, RoundingMode.HALF_UP);
ตรรกะการคำนวณ Forex
การคูณ/หารสลับตามทิศทางคู่สกุลเงิน รวมไว้ใน enum CurrencyPair
public class ForexCalculator {
private static final int INTERMEDIATE_SCALE = 30;
public static BigDecimal calculate(
BigDecimal sourceAmount, BigDecimal midRate, BigDecimal spread,
CurrencyPair pair, TransactionDirection direction) {
BigDecimal appliedRate = switch (direction) {
case BUY -> midRate.add(spread); // Ask rate
case SELL -> midRate.subtract(spread); // Bid rate
};
// สลับคูณ/หารตามทิศทางคู่สกุลเงิน (กลาง 30 หลัก)
BigDecimal rawAmount = pair.convert(sourceAmount, appliedRate, direction, INTERMEDIATE_SCALE);
// การปัดเศษตามสกุลเงินและทิศทาง (BUY=CEILING, SELL=DOWN)
int scale = CurrencyPrecision.getScale(pair.getTargetCurrency(direction));
RoundingMode rounding = CurrencyPrecision.getRoundingMode(direction);
return rawAmount.setScale(scale, rounding);
}
}
public enum CurrencyPair {
USD_JPY("USD", "JPY"), EUR_JPY("EUR", "JPY"), ...;
/** BUY (JPY→USD): หาร SELL (USD→JPY): คูณ */
public BigDecimal convert(BigDecimal amount, BigDecimal appliedRate,
TransactionDirection direction, int scale) {
return switch (direction) {
case BUY -> amount.divide(appliedRate, scale, RoundingMode.HALF_UP);
case SELL -> amount.multiply(appliedRate).setScale(scale, RoundingMode.HALF_UP);
};
}
}
Idempotency Key
Idempotency key ถูกนำมาใช้สำหรับฝาก/ถอน โอน และคำสั่งเพื่อให้การลองใหม่ปลอดภัย
- การส่งซ้ำด้วย key เดิมคืนผลลัพธ์เดิม
- ป้องกันการนับซ้ำ
นี่คือการใช้งานที่สำคัญที่บรรลุทั้งความพร้อมใช้งานที่สูงขึ้นและการรับประกันความสอดคล้องพร้อมกัน
Redis Rate Lock (QuoteStore)
ในโดเมน Forex การดึงใบเสนอราคาและการดำเนินการถูกแยกออก โดยตั้ง TTL บนใบเสนอราคา
@Component
public class RedisQuoteStore {
private static final Duration QUOTE_TTL = Duration.ofSeconds(30);
public void save(ForexQuote quote) {
// บันทึกใน Redis (TTL=30 วินาที)
redisTemplate.opsForValue().set("quote:" + quote.quoteId(), quote, QUOTE_TTL);
// บันทึกถาวรใน DB ด้วยสถานะ PENDING (สำหรับการสอบสวนของ customer support)
quoteRecordRepository.insert(quote.toRecord(QuoteStatus.PENDING));
}
/** ดึง Quote ด้วย QuoteID และลบจาก Redis (การรับประกันการใช้ครั้งเดียว) */
public ForexQuote getAndMarkUsed(String quoteId) {
ForexQuote quote = redisTemplate.opsForValue().getAndDelete("quote:" + quoteId);
if (quote == null) {
throw new QuoteExpiredException(quoteId);
}
quoteRecordRepository.updateStatus(quoteId, QuoteStatus.USED);
return quote;
}
}
การดำเนินการ Forex ใช้ Transactional Outbox pattern เพื่อรับประกัน atomicity
@Transactional
void executeExchange(String quoteId, String accountId) {
ForexQuote quote = quoteStore.getAndMarkUsed(quoteId); // 1. ดึง Quote ทำเป็น USED
accountService.debit(accountId, quote.sourceAmount()); // 2. หักจากบัญชีเยน
outboxRepository.insert(new OutboxMessage( // 3. บันทึก FX credit event
"ForeignCurrencyCredit", // ในธุรกรรมเดียวกัน
accountId, quote.targetCurrency(), quote.targetAmount()
));
forexTransactionRepository.save(quote.toTransaction()); // 4. บันทึกประวัติธุรกรรม
// การเครดิตบัญชี FX ดำเนินการแบบ async โดย Outbox Processor (สูงสุด 3 ครั้งลองใหม่)
}
⚠️ Warning: สิ่งนี้ขจัดสถานะ half-commit ของ “หัก JPY สำเร็จ → เครดิต FX ล้มเหลว”
ทำไมถึงเลือก Java 21 / Spring Boot 3
เหตุผลในการเลือกใช้ไม่ใช่ความใหม่ แต่เป็นความเหมาะสมกับการออกแบบ
- Java 21
- แสดง DTO และ VO ได้กระชับและไม่เปลี่ยนแปลงด้วย record (
Money,InterestRateใช้งานเป็น record) - ชี้แจงตรรกะการเปลี่ยนสถานะด้วย switch expression (การแยก BUY/SELL ใน
ForexCalculatorฯลฯ)
- แสดง DTO และ VO ได้กระชับและไม่เปลี่ยนแปลงด้วย record (
- Spring Boot 3
- สอดคล้องกับรากฐาน API ที่แยกโมดูล
- การดำเนินการรวมของ security, batch และการเข้าถึงข้อมูล
- Virtual Threads
- ห้องสำหรับปรับปรุง throughput ในการประมวลผล API ที่ผูกกับ I/O
Record โดยเฉพาะมีประสิทธิภาพในการแมปเจตนาการออกแบบโดยตรงไปยังโค้ด
🚀 ระบบตรวจสอบ PoC
ทำไมถึงเป็น Vanilla HTML และ nginx
วัตถุประสงค์ของ PoC ไม่ใช่การสร้าง UI ที่สวยงาม แต่เป็นการหมุนเวียนการตรวจสอบสัญญา API อย่างรวดเร็ว
ดังนั้นจึงนำการกำหนดค่าต่อไปนี้มาใช้:
- Docker Compose
- nginx
- การแยกหน้าจอภายนอกและภายใน
- Vanilla HTML และ JavaScript ขั้นต่ำ
การกำหนดค่าระบบทั้งหมดจริง:
┌──────────────────────────────────────────────────────┐
│ เครื่อง Local ของนักพัฒนา (host เดียว) │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Docker Compose Network (banklink-net) │ │
│ │ │ │
│ │ ┌────────────────┐ ┌────────────────────┐ │ │
│ │ │ UI ภายนอก │ │ UI ภายใน │ │ │
│ │ │ (Nginx:8080) │ │ (Nginx:8081) │ │ │
│ │ └───────┬────────┘ └────────┬───────────┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ banklink-app (8443) │ │ │
│ │ │ OAuth2 Resource Server │ │ │
│ │ │ /v1/accounts /v1/loans │ │ │
│ │ │ /v1/forex /v1/investments │ │ │
│ │ │ /api/v1/kyc │ │ │
│ │ └──────────────┬──────────────────────┘ │ │
│ │ │ │ │
│ │ ┌──────────┴───────────┐ │ │
│ │ ▼ ▼ │ │
│ │ ┌────────────┐ ┌──────────────────────┐ │ │
│ │ │ PostgreSQL │ │ Redis (6379) │ │ │
│ │ │ (5432) │ │ Idempotency / Cache │ │ │
│ │ └────────────┘ └──────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────┐ │ │
│ │ │ Batch Runner (ดำเนินการด้วยตนเอง) │ │ │
│ │ │ - Batch ดำเนินการคำสั่ง │ │ │
│ │ │ - Batch อัปเดตช่อง NISA │ │ │
│ │ │ - Batch ค่าปรับความล่าช้า │ │ │
│ │ └────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ Browser: http://localhost:8080 (UI ภายนอก) │
│ Browser: http://localhost:8081 (UI ภายใน) │
└──────────────────────────────────────────────────────┘
UI ภายนอกเป็นหน้าจอการดำเนินการ API สำหรับผู้ใช้ทั่วไป UI ภายในสำหรับพนักงานธนาคารเช่นผู้ตรวจสอบ KYC ทั้งสองเชื่อมต่อกับ API server เดียวกัน แต่มีเส้นทางการเข้าถึงแยกต่างหากผ่าน nginx proxy เพื่อตรวจสอบขอบเขตสิทธิ์ในสภาพแวดล้อมจริง
ข้อดีของการตั้งค่านี้คือการเปลี่ยนหน้าจอสะท้อนทันทีและการทดสอบ API มีระยะเวลาสั้น
หน้าจอ Client ภายนอก (สำหรับผู้ใช้):

สิ่งที่ Phase 1 ตรวจสอบได้
- การเชื่อมต่อ API หลักใน 5 โดเมน
- การดำเนินการพื้นฐานของขอบเขตสิทธิ์
- การตรวจสอบ audit/trace ขั้นพื้นฐาน
- การหมุนเวียนการทดสอบผ่าน dev auxiliary API
หน้าจอตรวจสอบการดำเนินการ (UI ภายนอก)
การตรวจสอบยอดคงเหลือบัญชีและการดึงประวัติธุรกรรม:

การจำลองการชำระคืน (level payment, 60 งวด):

การดึงใบเสนอราคา Forex (JPY→USD, 10,000 เยน):

การตรวจสอบพอร์ตโฟลิโอและการยืนยันช่อง NISA:

หน้าจอตรวจสอบการดำเนินการ (UI ภายใน / พนักงานธนาคาร)
คิว KYC ที่รอการตรวจสอบและแบบฟอร์มตรวจสอบ:

ปัญหาที่พบในการตรวจสอบ (การปรับ scope เชิงบวก)
ช่องว่างที่พบจากการตรวจสอบข้อเท็จจริงสามารถจัดระเบียบได้ไม่ใช่เป็นความล้มเหลว แต่เป็นการแบ่งขอบเขต Phase 1 ที่ตั้งใจ
- API การชำระคืนตามกำหนดสินเชื่อมีอยู่ฝั่งการออกแบบ แต่การใช้งานเป็น phase ถัดไป
- การแสดง KYC path มีความไม่สอดคล้องระหว่างการใช้งานและการแสดงผ่าน proxy
- หน้าจอ PoC ภายนอกมุ่งเน้นการดำเนินการหลัก ยังไม่บรรลุการครอบคลุม UI เต็มรูปแบบ
- Batch processing ใน Phase 1 บางส่วนใช้การใช้งานที่เน้น log; business logic หลักจะถูกนำมาใช้แบบค่อยเป็นค่อยไป
📝 Note: สิ่งนี้แยกโดยเจตนาว่าอะไรจะตรวจสอบใน PoC จากสิ่งที่จะขัดเกลาเพื่อความพร้อมสำหรับ production
🗺️ แผนงาน: เส้นทางสู่ความพร้อมสำหรับ Production
Phase 2 — การเสริมความปลอดภัย
- การนำ API Gateway / WAF มาใช้
- การรวม auth infrastructure สำหรับ production (Keycloak / Auth0)
- เอกสาร OpenAPI และการเสริม E2E
เป้าหมาย: ยกระดับการป้องกันขอบเขตและความสามารถในการดำเนินงาน
Phase 3 — การรวม Async เต็มรูปแบบ
- Outbox relay ในการดำเนินงาน production
- การขยาย event integration ระหว่างโดเมน (บัญชี ↔ สินเชื่อ ↔ การลงทุน)
- การจัดทำเอกสาร eventual consistency และกลยุทธ์การลองใหม่
เป้าหมาย: ปรับขนาด integration ระหว่างโมดูลอย่างปลอดภัย
Phase 4 — Batch Processing สำหรับการดำเนินธุรกิจ
- การใช้งานเต็มรูปแบบของ batch การชำระคืนสินเชื่อตามกำหนด
- การใช้งานเต็มรูปแบบของการคำนวณค่าปรับความล่าช้า
- การใช้งานเต็มรูปแบบของการประมวลผล NISA ประจำปี
- Batch การทำให้ไม่ระบุตัวตน (ลบ PII สำหรับลูกค้าที่ปิดบัญชีเกิน 10 ปี)
เป้าหมาย: ทำให้การทำซ้ำของการดำเนินงานรายวัน/รายเดือนเป็นอัตโนมัติ
Phase 5 — การย้ายไปยังการกำหนดค่า Production
- การย้ายไปยัง managed DB/cache
- การเสริม observability
- การใช้งาน high availability และ disaster recovery
เป้าหมาย: ยกระดับจากการกำหนดค่าที่สะดวกสำหรับการพัฒนาไปยังการกำหนดค่าที่รองรับความต่อเนื่องทางธุรกิจ
สรุป
สิ่งที่ให้ความสำคัญในความพยายามนี้คือความสามารถในการตรวจสอบการออกแบบมากกว่าความเร็วในการใช้งาน
| Phase | สิ่งที่เราทำ |
|---|---|
| การกำหนดความต้องการ | กำหนดข้อจำกัดก่อน (จัดทำเอกสาร actors, state transitions, การกำหนดค่าการคำนวณ) |
| การออกแบบขั้นพื้นฐาน | บังคับขอบเขตด้วยการทดสอบ (ArchUnit, ER diagram, การออกแบบ API endpoint) |
| การออกแบบรายละเอียด | แปลคุณภาพทางการเงินไปยังโค้ด (BigDecimal, pessimistic locking, Outbox, idempotency) |
| PoC | แสดงช่องว่างจริงแต่เนิ่นๆ |
| Phase ถัดไป | เชื่อมต่อช่องว่างไปยังแผนงานแทนที่จะซ่อนเป็นหนี้ |
ในโดเมนที่มีความน่าเชื่อถือสูงเช่นธนาคาร การมุ่งสู่ความสมบูรณ์ครั้งเดียวจริงๆ ช้ากว่าการเสริมความแข็งแกร่งทีละขั้นด้วยเหตุผลที่ชัดเจน
การออกแบบก่อนไม่ใช่การอ้อมค้อม แต่เป็นเส้นทางที่สั้นที่สุดในการรักษาความสามารถในการเปลี่ยนแปลงจนถึงวินาทีสุดท้าย