Tech Blog

การสร้าง Banking API ด้วยการออกแบบที่มาก่อน: จากการกำหนดความต้องการ 5 โดเมนสู่การตรวจสอบ PoC และแผนงานสู่ Production

by Tech Writer
Java Spring Boot DDD Finance Design PostgreSQL Redis Docker ArchUnit

สิ่งที่คุณจะได้เรียนรู้

  • วิธีจัดการข้อจำกัดที่ห้ามผิดพลาดในการออกแบบ 5 โดเมน: บัญชี สินเชื่อ อัตราแลกเปลี่ยน การลงทุน และ KYC
  • วิธีบังคับขอบเขตโดเมนด้วย Modular Monolith (รวม ArchUnit)
  • การใช้ BigDecimal, Pessimistic Locking, Transactional Outbox และ Idempotency Key เพื่อคุณภาพระดับการเงิน
  • สิ่งที่ PoC เผยให้เห็นและเชื่อมต่อกับขั้นตอนถัดไปอย่างไร

กลุ่มเป้าหมาย

  • นักพัฒนาที่สนใจการออกแบบและการใช้งาน API สำหรับระบบการเงิน/ฟินเทค
  • ผู้ที่ต้องการฝึกฝน DDD / การออกแบบโดเมนโมเดล
  • Backend engineer ที่ต้องการเข้าใจเหตุผลเบื้องหลังการออกแบบของ Spring Boot + Java

สภาพแวดล้อม

รายการเวอร์ชัน
Java21 (Virtual Threads)
Spring Boot3.x
PostgreSQL16
Redis7
Build ToolMaven (multi-module)
ContainerDocker Compose

บทนำ

เมื่อใช้งานระบบธนาคาร ความท้าทายแรกไม่ใช่ความกว้างของฟีเจอร์ แต่เป็นวิธีจัดการพื้นที่ที่ความล้มเหลวไม่เป็นที่ยอมรับ

  • ห้ามมีข้อผิดพลาดในการคำนวณ
  • ห้ามมีการประมวลผลซ้ำ
  • ต้องมีความสามารถในการตรวจสอบ
  • ต้องรองรับการแยกหรือขยายในอนาคต

บทความนี้อธิบายกระบวนการสร้าง Banking API ใน 5 โดเมน ได้แก่ บัญชี สินเชื่อ อัตราแลกเปลี่ยน การลงทุน และ KYC โดยมีโครงสร้างตาม 3 ขั้นตอนการออกแบบ

แกนหลักคือ:

  1. การกำหนดความต้องการ
  2. การออกแบบขั้นพื้นฐาน
  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 + SpreadSourceAmount ÷ AppliedRate
ขาย FX (USD → JPY)1 USD = 151.50 JPY (Bid)MidRate - SpreadSourceAmount × AppliedRate

🚨 Alert: การสลับทิศทางการคำนวณจะทำให้กำไรและขาดทุนกลับด้าน CurrencyPair ถูกกำหนดเป็น enum ที่รวมการคูณ/หารเพื่อป้องกันข้อผิดพลาด

การปัดเศษตามสกุลเงิน (มุมมองกำไรธนาคาร)

สกุลเงินการปัดเศษ BUYการปัดเศษ SELL
JPYCEILING (ปัดขึ้น)DOWN (ตัดทิ้ง)
USD / EUR / GBPCEILINGDOWN

เมื่อลูกค้าซื้อสกุลเงินต่างประเทศ การปัดขึ้นหมายถึงลูกค้าจ่ายเล็กน้อยมากขึ้น เมื่อขาย การตัดทิ้งหมายถึงลูกค้าได้รับเล็กน้อยน้อยลง — สะสมกำไรเล็กน้อยสำหรับธนาคาร

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 การจัดการบัญชี

MethodPathคำอธิบาย
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 สินเชื่อ

MethodPathคำอธิบาย
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 อัตราแลกเปลี่ยน

MethodPathคำอธิบาย
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 การลงทุน

MethodPathคำอธิบาย
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

MethodPathคำอธิบาย
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 การจัดเก็บยอดคงเหลือหลังธุรกรรมเป็น snapshot balance_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 ฯลฯ)
  • 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 ภายนอก (สำหรับผู้ใช้):

หน้าจอ Client ภายนอก

สิ่งที่ Phase 1 ตรวจสอบได้

  • การเชื่อมต่อ API หลักใน 5 โดเมน
  • การดำเนินการพื้นฐานของขอบเขตสิทธิ์
  • การตรวจสอบ audit/trace ขั้นพื้นฐาน
  • การหมุนเวียนการทดสอบผ่าน dev auxiliary API

หน้าจอตรวจสอบการดำเนินการ (UI ภายนอก)

การตรวจสอบยอดคงเหลือบัญชีและการดึงประวัติธุรกรรม:

การตรวจสอบ API บัญชี

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

การจำลองการชำระคืนสินเชื่อ

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

การดึงใบเสนอราคา Forex

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

การยืนยันการลงทุนและช่อง NISA

หน้าจอตรวจสอบการดำเนินการ (UI ภายใน / พนักงานธนาคาร)

คิว KYC ที่รอการตรวจสอบและแบบฟอร์มตรวจสอบ:

หน้าจอตรวจสอบ KYC (UI ภายใน)

ปัญหาที่พบในการตรวจสอบ (การปรับ scope เชิงบวก)

ช่องว่างที่พบจากการตรวจสอบข้อเท็จจริงสามารถจัดระเบียบได้ไม่ใช่เป็นความล้มเหลว แต่เป็นการแบ่งขอบเขต Phase 1 ที่ตั้งใจ

  1. API การชำระคืนตามกำหนดสินเชื่อมีอยู่ฝั่งการออกแบบ แต่การใช้งานเป็น phase ถัดไป
  2. การแสดง KYC path มีความไม่สอดคล้องระหว่างการใช้งานและการแสดงผ่าน proxy
  3. หน้าจอ PoC ภายนอกมุ่งเน้นการดำเนินการหลัก ยังไม่บรรลุการครอบคลุม UI เต็มรูปแบบ
  4. 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 ถัดไปเชื่อมต่อช่องว่างไปยังแผนงานแทนที่จะซ่อนเป็นหนี้

ในโดเมนที่มีความน่าเชื่อถือสูงเช่นธนาคาร การมุ่งสู่ความสมบูรณ์ครั้งเดียวจริงๆ ช้ากว่าการเสริมความแข็งแกร่งทีละขั้นด้วยเหตุผลที่ชัดเจน

การออกแบบก่อนไม่ใช่การอ้อมค้อม แต่เป็นเส้นทางที่สั้นที่สุดในการรักษาความสามารถในการเปลี่ยนแปลงจนถึงวินาทีสุดท้าย

ส่งข้อความได้ตามสบาย

กรุณาส่งข้อความ หากมีคำปรึกษาด้านเทคนิค ความคิดเห็น หรือคำถาม