บทที่ 23 · Part 5 — Enterprise Fintech Architecture
Web Portal, BFF and Read Composition
ออกแบบ Portal เป็น channel ที่ compose reads และส่ง commands กลับ owning context โดยไม่ join authoritative tables
Web Portal, BFF and Read Composition
Web Portal ต้องแสดง Wallet balance, recent Checkout payments และ Billing invoices ในหน้าเดียว ทางลัดคือ BFF join tables ของ 3 services แล้วเพิ่ม endpoint Approve Refund ที่ update Billing table โดยตรง Portal กลายเป็น shadow domain owner และ coupling hub ทั้งที่จุดประสงค์เดิม คือปรับข้อมูลให้เหมาะกับ browser
จบบทนี้คุณจะ
ออกแบบ Web Portal เป็น channel/BFF แยก command path จาก composed read path วาง browser session/coarse enforcement และ freshness metadata หลีกเลี่ยง cross-context database joins พร้อมป้องกัน display financial state ถูกนำไปใช้เป็น authoritative decision
[!IMPORTANT] วิธีอ่านหลักฐานในบทนี้ คำที่เป็น Definition/Pattern มีแหล่งต้นทางอยู่ใกล้ claim ส่วน decision table, starting structure, checklist และ lab เป็น Heuristic/Trade-off หรือ Course Convention สำหรับฝึกตัดสินใจ ไม่ใช่มาตรฐานสากล เว้นแต่บทจะระบุแหล่งและขอบเขตไว้ชัดเจน
เริ่มต้นให้ Portal เป็น Channel
Sam Newman อธิบาย Backends for Frontends ว่า backend เฉพาะ user experience/client type ช่วย tailor interaction BFF อาจ handle session, aggregation และ protocol adaptation แต่ไม่ควรยึด domain invariants จาก services/context owners
starting responsibilities:
- browser-facing session/token handling และ CSRF/security headers ตาม architecture
- map UI request เป็น application commands
- coarse permission เพื่อ UX/early rejection แต่ owner re-authorize
- compose read models และ attach freshness/partial status
- hide internal topology/version differences จาก browser
หาก Portal มี workflow/language/rules เฉพาะจริง เช่น case-management lifecycle ให้ discovery และอาจสร้าง context ของมัน ไม่ยึด hypothesis เดิมโดยอัตโนมัติ
แยกเส้นทาง Command และ Query
Query path optimize latency/shape และยอม stale/partial ตาม product requirement Command path ส่ง identifiers กับ intent ไป owner ซึ่งอ่าน authoritative state และตรวจ authorization/invariant ใหม่
อย่าส่ง displayed_balance เป็น proof ว่า transfer ทำได้ อาจส่ง expected version เพื่อ detect
user decision จากหน้าจอเก่า แต่ owner ยังต้อง re-evaluate
Display balance ไม่ใช่ decision balance
BFF/Redis cache อาจแสดงค่า asOf แต่ Transfer/Refund approval ต้องให้ owning context อ่าน
authoritative state ใน concurrency boundary และคืน outcome ใหม่ UI ต้องรองรับ conflict/
state changed แทนการบังคับค่าที่ผู้ใช้เห็นก่อนหน้า
ทางเลือกในการประกอบ Read Model
- Runtime API composition — BFF เรียก context query APIs parallel มี freshness ล่าสุดแต่ latency/failure fan-out
- Replicated read model — consume events สร้าง Portal view เร็วและ resilient ต่อ owner outage แต่ eventual consistency/schema ownership ซับซ้อน
- Dedicated query service — มี owner/contract ชัดเมื่อ composition เป็น capability ใหญ่
- GraphQL/API gateway composition — technology ช่วย shape แต่ไม่แก้ data ownership เอง
เลือกต่อ screen/use case ไม่จำเป็นต้องแบบเดียวทั้ง Portal ให้ response ระบุ generated_at,
source status และ partial errors โดยไม่ expose internal sensitive topology
ห้าม BFF join MySQL schemas ข้าม contexts โดยตรง เพราะ bypass owner access rules, schema contracts และ audit หากจำเป็นช่วง migration ต้องมี ADR, read-only scope, owner และ expiry
Authentication และ Authorization บน Browser
BFF อาจเป็น confidential client/session boundary ลด token exposure ใน browser ตาม chosen architecture แต่ต้องออกแบบ cookie attributes, CSRF, session fixation, token rotation, logout และ origin controls จาก threat model
Portal ตรวจ menu/action visibility เพื่อ UX แต่ server owner ยัง deny-by-default ทุก command Principal mapping หยุด protocol details และ propagate tenant/subject/authn context อย่าง authenticated ห้ามเชื่อ tenant ID จาก editable request body
Contract ของหน้าจอ Portal
{
"customer_id": "cus_123",
"wallet": {
"display_available_minor": 125000,
"currency": "THB",
"as_of": "2026-08-08T10:00:00Z"
},
"recent_payments": [],
"open_invoices": [],
"source_status": {
"wallet": "fresh",
"checkout": "partial",
"billing": "fresh"
}
}
จำนวนเงินเป็น integer minor units Source status ทำให้ UI ไม่แสดง partial data เป็น complete truth แต่ exact freshness/SLA ต้องมาจาก product/owner ไม่ใช่ค่าตัวอย่าง
Approve Refund request ส่ง refund_id, intended action และ optional expected version
ไม่ส่ง provider status หรือ current refundable amount ให้ Portal ตัดสิน
แบบฝึกปฏิบัติ: รวมข้อมูลโดยไม่ยึด Ownership
ออกแบบ Account Overview screen:
UI field:
Owning context/query contract:
Authoritative or display-only:
Freshness/partial behavior:
Sensitive-data rule:
Fallback when owner unavailable:
Command launched from field:
Owning command endpoint and rechecks:
จำลอง Wallet query timeout, Billing stale event และ user กด Approve หลัง Charge เปลี่ยน state ระบุ UX กับ server outcome โดยไม่ใช้ cross-context DB join
รายการตรวจสอบ
- Portal/BFF เริ่มเป็น channel ไม่ใช่ domain owner
- Command กับ query paths แยก responsibility
- Read composition ผ่าน owner contracts หรือ owned replica
- Response บอก freshness/partial semantics
- Financial command re-read authoritative state
- Browser auth/session มี threat model และ server re-authorization
- Cross-context DB join ถูกห้ามหรือมี temporary ADR/expiry
- Portal-specific domain ถูกสร้างเมื่อมี evidence จริงเท่านั้น
สรุปบทนี้
BFF มีคุณค่าเมื่อปรับ contract และ composition ให้ client แต่ต้องรักษา ownership ของ contexts Query view อาจ stale/partial ได้ตาม contract ขณะที่ command กลับไป owner เพื่อตัดสินใหม่ การแยก display กับ decision ทำให้ Portal ไม่กลายเป็น shadow financial system
อ่านเพิ่มเติม
- Backends for Frontends — original pattern by Sam Newman
- OWASP Session Management Cheat Sheet
- OWASP CSRF Prevention Cheat Sheet
- DDD Reference — context ownership และ Published Language