🚗
← Secure by Design

SECURE BY DESIGN DAY 11

เริ่มทำควิซ ⚡
Part 3: Applying the Fundamentals | Chapter 11 (Intermission)

An Insurance Policy for Free

Microservices Myopia & Semantic Breach 🚗💥
📌 TITLE ANCHOR QUESTION ทำไมบทนี้ถึงชื่อนี้? และเกิดอะไรขึ้นกับระบบประกันภัย?

คำตอบและหมุดเชื่อมโยง: คำว่า "An insurance policy for free" (กรมธรรม์ประกันภัยฟรีไม่มีค่าใช้จ่าย) มาจากคดีความจริงในชั้นศาล เมื่อบริษัทประกันภัยแห่งหนึ่งถูกลูกค้าฟ้องร้องให้ชดใช้ค่าเสียหายจากอุบัติเหตุรถชน ทั้งๆ ที่ลูกค้า "ยังไม่ได้ชำระเงินค่าเบี้ยประกันแม้แต่บาทเดียว" แต่กลับมีหนังสือรับรองกรมธรรม์ฉบับจริงส่งไปถึงบ้านของลูกค้าแล้ว!

ผู้เขียนตั้งชื่อบทนี้เพื่อชี้ให้เห็นสภาวะอันตรายที่สุดของการสถาปัตยกรรม Microservices คือ "ระบบย่อยทุกระบบทำงานถูกต้อง 100% ตาม Specification ของตัวเอง แต่เมื่อนำมารวมกันเป็น System of Systems ระบบรวมกลับพังทลายลงสิ้นเชิง" สาเหตุเกิดจาก Microservices Myopia (การมองไมโครเซอร์วิสแบบคับแคบ) เมื่อแต่ละทีมตีความคำว่า Payment (การชำระเงิน) ข้าม Bounded Context แตกต่างกัน จนนำไปสู่ช่องโหว่ทางตรรกะที่ออกสัญญาคุ้มครองให้ลูกค้าไปฟรีๆ ครับ!

Part 1 & 2 ที่ผ่านมา (Recap)

Days 1-7: รากฐาน DDD, Immutability, Domain Primitives, State Integrity และ Entity Relay

ตำแหน่งปัจจุบัน (Where We Are)

Day 11 จาก 14 วัน — บทพักครึ่งวิเคราะห์กรณีศึกษาระบบประกันภัย ก่อนเข้าสู่ Legacy Code และ Microservices

เป้าหมายประจำวัน (Core Mission)

เข้าใจภัยเงียบของ Microservices Myopia, การตีความความหมายข้าม Bounded Context และใช้ Context Mapping กอบกู้ระบบ

02

เรื่องราวและมโนทัศน์เชิงลึก (The Insurance Case & Semantic Architecture)

🏢 SCENARIO 2.1 • วิวัฒนาการจาก MONOLITH สู่ MICROSERVICES และจุดเริ่มของความเข้าใจผิด Architectural Evolution

🏛️ ยุค Monolith ดั้งเดิม (เงินสดหน้าร้าน)

ในอดีต บริษัทประกันภัยดำเนินธุรกิจแบบ Over-the-Counter ลูกค้าเดินเข้ามาที่สาขา ยื่นเอกสาร และ "จ่ายเงินสดชำระเบี้ยประกันที่เคาน์เตอร์" พนักงานบันทึกข้อมูลเข้าระบบ Monolith เมื่อระบบได้รับ Event Payment มันจะสร้าง PolicyPeriod (ขยายเวลาคุ้มครอง) และพิมพ์หนังสือรับรองส่งทางไปรษณีย์ทันที

📌 ความหมายใน Monolith: คำว่า Payment มีความหมายเดียวอย่างชัดเจน คือ "ได้รับเงินสดครบถ้วน 100% แล้ว"

✂️ การแยกออกเป็น 2 Microservices

เมื่อองค์กรเติบโต ระบบเริ่มใหญ่เกินไป ทีมงานจึงแยก Monolith ออกเป็น 2 บริการตาม Bounded Context:

1. Finance Service (การเงิน): ดูแลเรื่องการรับเงิน, บัญชี, และจ่ายค่าซ่อมให้อู่
2. Policy Service (กรมธรรม์): ดูแลเงื่อนไขกรมธรรม์ และพิมพ์เอกสารคุ้มครองส่งลูกค้า
⚡ เมื่อ Finance ได้รับเงิน จะส่ง Event Payment ข้าม Message Queue ไปหา Policy เพื่อสั่งพิมพ์จดหมาย ซึ่งในระยะแรก ทุกอย่างยังทำงานถูกต้องดี
💣 SCENARIO 2.2 • การเปิดรับ BANK GIRO, ความหมายที่เปลี่ยนไป และคดีความในศาล Semantic Collision

🏦 ช่องทางใหม่: หักบัญชีอัตโนมัติ (Bank Giro)

ฝ่ายการตลาดเพิ่มช่องทางชำระเงินผ่าน Bank Giro (Direct Debit) โดยระบบธนาคารจะส่ง Message 3 ชนิดมาให้ Finance Service:

1. Payment: แปลว่า "ธนาคารได้รับคำขอหักเงินแล้ว (Registered)" (ยังไม่ได้เงิน!)
2. Confirm: แปลว่า "หักเงินสำเร็จเรียบร้อยแล้ว (Transferred)"
3. Bounce: แปลว่า "หักเงินไม่สำเร็จ เงินในบัญชีไม่พอ (Failed)"
💥 จุดระเบิดความเข้าใจผิด: ทีม Finance เห็น Message คำว่า Payment จากธนาคาร จึงนำไป Map เข้ากับ Event Payment เดิมของตนเองแล้วยิงต่อให้ Policy ทันที! ฝั่ง Policy คิดว่าลูกค้าจ่ายเงินสดแล้ว จึงพิมพ์หนังสือคุ้มครองส่งไปบ้านลูกค้าทันที!

🚗💥 อุบัติเหตุรถชน และคำพิพากษาศาล

นาย B สมัครหักเงินผ่าน Bank Giro ถึงวันต่ออายุ ธนาคารส่ง Payment (แจ้งรับเรื่อง) ให้ Finance -> Finance ยิงต่อให้ Policy -> Policy พิมพ์หนังสือคุ้มครองส่งไปบ้านนาย B

ความจริงคือนาย B ไม่มีเงินในบัญชี! ธนาคารส่ง Message Bounce มาบอกว่าหักเงินล้มเหลว แต่ Finance เมินเฉยเพราะรอฟังแค่ Payment!

ต่อมานาย B ขับรถชนต้นไม้พังยับเยิน ถือหนังสือคุ้มครองมาเคลมประกัน บริษัทปฏิเสธว่า "คุณยังไม่ได้จ่ายเงิน!" นาย B ฟ้องศาลทันที

⚖️ คำตัดสินของศาล: บริษัทประกันภัยต้องชดใช้ค่าเสียหายทั้งหมด! ศาลชี้ว่าการส่งหนังสือรับรองไปให้ลูกค้า ถือว่าบริษัทได้แสดงเจตนาตกลงให้สัญญามีผลผูกพันทางกฎหมายแล้ว (De Facto Business Practice) ส่วนเรื่องเงินที่ยังหักไม่ได้ ถือเป็นหนี้ค้างชำระที่บริษัทต้องไปไล่เบี้ยเอาเอง!
🗺️ SCENARIO 2.3 • สยบ MICROSERVICES MYOPIA ด้วย CONTEXT MAPPING & EXPLICIT DOMAIN EVENTS Deliberate Discovery

🔍 ใครเป็นคนสร้าง Bug นี้ขึ้นมา?

ความจริงที่น่าตกใจคือ "ไม่มีโค้ดของบริการไหนพังเลยสักบริการเดียว!"

• Bank Giro ทำงานถูกต้องตามมาตรฐานของธนาคาร
• Finance Service ทำงานถูกต้องตามอัลกอริทึมของทีมการเงิน
• Policy Service พิมพ์เอกสารออกไปถูกต้องตาม Event ที่ได้รับ
💥 แต่ระบบรวมพัง เพราะเกิด "Microservices Myopia (การมองแบบคับแคบ)" ต่างคนต่างคิดว่าตัวเองทำถูกต้อง โดยไม่พูดคุยความหมายทางธุรกิจข้ามบริการ!

🛡️ การแก้ไขด้วย Context Mapping

จัด Workshop นำ Domain Experts ของทุกฝ่ายมาคุยกัน (Deliberate Discovery) แล้วสร้าง Context Map ประสานความหมายใหม่:

1. แยกศัพท์ให้ชัดเจน: ไม่ใช้คำว่า Payment ลอยๆ แต่แยกเป็น CashPayment (ได้เงินสดแล้ว) และ GiroPayment (อยู่ระหว่างขอหักเงิน)
2. รอ Confirm เท่านั้น: Finance Service จะยิง Event PolicyPaymentReceived ไปหา Policy Service ก็ต่อเมื่อได้รับ Message Confirm จากธนาคารเท่านั้น!
EXPLICIT DOMAIN EVENT FLOW (CONTEXT MAPPING RESOLUTION)
1
BANK GIRO DOMAIN

ส่ง Message: Payment (ลงทะเบียน), Confirm (หักเงินสำเร็จ), หรือ Bounce (เงินไม่พอ)

2
FINANCE DOMAIN

ถ้าเจอ Payment ➔ บันทึกรอ / ถ้าเจอ Bounce ➔ ยกเลิก / ถ้าเจอ Confirm ➔ ยิง Event ยืนยันเงินเข้า

3
POLICY DOMAIN

รอรับเฉพาะ Event PolicyPaymentReceived เท่านั้น จึงจะออกใบรับรองกรมธรรม์ คุ้มครองอย่างปลอดภัย 100%!

03

แบบฝึกหัดปฏิบัติตามสถานการณ์จริง (Hands-on Refactoring)

Before: Naive Event Forwarding (ตีความผิดบริบท) เสี่ยงแจกประกันฟรี
// โค้ดเดิมฝั่ง Finance: ตีความ Message "Payment" จากธนาคารผิดอย่างมหันต์
public class BankGiroIntegrationListener {
    private EventPublisher eventPublisher;

    public void onBankGiroMessage(BankGiroMessage message) {
        // 💥 ช่องโหว่ร้ายแรง: Message "Payment" ของธนาคารเป็นแค่การ "ลงทะเบียนคำขอ"
        // แต่ดันยิง Event "PaymentCompleted" ไปหลอก Policy Service ว่าได้รับเงินแล้ว!
        if ("Payment".equals(message.getType())) {
            eventPublisher.publish(new PaymentCompletedEvent(
                message.getPolicyId(), 
                message.getAmount()
            ));
        }
        // 💥 Message "Confirm" และ "Bounce" ถูกเพิกเฉยทิ้งไปเลย!
    }
}
⚠️ จุดพังของระบบ: เมื่อลูกค้าไม่มีเงินในบัญชี ธนาคารส่ง Message Bounce มา แต่ระบบไม่สนใจ Policy Service ส่งหนังสือคุ้มครองไปถึงบ้านลูกค้าแล้ว ศาลจึงสั่งให้บริษัทต้องจ่ายค่าสินไหมรถชนฟรี!
After: Explicit States & Confirmed Event Flow Zero Semantic Confusion
// โค้ดใหม่ฝั่ง Finance: แยกแยะสถานะ Bank Giro อย่างชัดเจนตาม Bounded Context
public class BankGiroIntegrationListener {
    private final EventPublisher eventPublisher;
    private final GiroTransactionRepository giroRepository;

    public BankGiroIntegrationListener(EventPublisher ep, GiroTransactionRepository repo) {
        this.eventPublisher = notNull(ep);
        this.giroRepository = notNull(repo);
    }

    public void onBankGiroMessage(BankGiroMessage message) {
        notNull(message, "BankGiroMessage required");

        switch (message.getType()) {
            case "Payment":
                // 1. แค่บันทึกคำขอรอดำเนินการ ห้ามยิง Event แจ้งว่าได้เงินแล้วเด็ดขาด!
                giroRepository.recordPendingGiro(message.getTransactionId(), message.getPolicyId());
                break;

            case "Confirm":
                // 2. หักเงินสำเร็จจริงแล้ว จึงยิง Event ข้าม Bounded Context!
                GiroTransaction tx = giroRepository.markAsConfirmed(message.getTransactionId());
                eventPublisher.publish(new PolicyPaymentReceivedEvent(tx.policyId(), tx.amount()));
                break;

            case "Bounce":
                // 3. เงินในบัญชีไม่พอ บันทึกล้มเหลว ไม่มีการออกกรมธรรม์
                giroRepository.markAsFailed(message.getTransactionId());
                break;
        }
    }
}
🛡️ จุดเปลี่ยนสำคัญ: Policy Service จะไม่ได้รับ Event แจ้งให้สร้างกรมธรรม์จนกว่า Finance จะได้รับ Message Confirm จากธนาคารจริงๆ ปิดช่องโหว่การแจกกรมธรรม์ประกันภัยฟรี 100%!
04

ชุดควิซประเมินความรู้ประจำวัน

ตอบปุ๊บ รู้ผลเฉลยทันที พร้อมคำอธิบายและหลอดสะสมพลังงาน Energy Charge

Energy Charge
0 / 10
📌 CLOSING TITLE CONNECTION WRAP-UP สรุปหมุดเชื่อมโยงกลับหาหัวข้อบท

สรุปการเชื่อมโยงกลับสู่ Chapter 11 — Intermission: An insurance policy for free

คดีความกรมธรรม์ประกันภัยรถชนฟรีในวันนี้ เป็นอุทาหรณ์ที่เด่นชัดของหัวข้อบท "Intermission: An insurance policy for free" อย่างสมบูรณ์แบบว่า เมื่อเราแยกระบบออกเป็น Microservices โดยขาดการทำ Context Mapping ที่ดี ความหมายของข้อมูลที่สะดุดข้าม Bounded Context จะกลายเป็นช่องโหว่ที่แจกสินค้าหรือบริการให้ผู้ใช้ไปฟรีๆ โดยที่โค้ดของแต่ละบริการไม่ได้มีบั๊กทางเทคนิคเลยแม้แต่บรรทัดเดียว! การดึง Domain Experts ทุกฝั่งมาร่วมออกแบบความหมายข้ามระบบและใช้ Explicit Domain Events จึงเป็นเกราะคุ้มกันเดียวที่จะป้องกันโศกนาฏกรรมเช่นนี้ได้ครับ!

พร้อมก้าวสู่การประยุกต์ใช้กับระบบเดิมแสนบรรทัดในบทถัดไปแล้วหรือยัง? Next: Day 12 — Chapter 12: Guidance in legacy code (สกัด Domain Primitives ในโค้ดเก่า) ➔