Skip to content

บทที่ 5 — Event-Driven Architecture

← บทที่ 4 | สารบัญ | บทที่ 6 →

หลังจบบทนี้ คุณจะสามารถ:

  • ออกแบบ event-driven system ตั้งแต่ศูนย์
  • ใช้ Kafka ได้อย่างถูกหลัก ไม่หลงกับดักที่มือใหม่มักเจอ
  • เข้าใจ event sourcing + CQRS (ซี-คิว-อาร์-เอส = Command Query Responsibility Segregation — แยก model ของอ่านกับเขียน) ในระดับลึก
  • ทำ stream processing สำหรับงาน real-time

1. Event-Driven Architecture (EDA) — สถาปัตยกรรมขับเคลื่อนด้วย event

แทนที่ service จะเรียกหากันตรง ๆ EDA ให้แต่ละ service "ประกาศ event" เมื่อ state เปลี่ยน แล้ว service อื่นที่สนใจก็ subscribe มาทำงานต่อ ผลคือระบบที่ decoupled (ไม่รู้จักกันตรง ๆ), async, มีหลาย consumer ต่อ event และ replay ได้เพราะ event ถูกบันทึกไว้:

Properties (คุณสมบัติ):

  • Decoupled (ดี-คับ-เปิ้ลด์ = ไม่ผูกติด) — service ไม่เรียกหากันตรง ๆ
  • Async — ไม่ต้องรอกัน
  • Multiple consumers per event — 1 event มีได้หลายผู้รับ
  • Auditable (อ๊อ-ดิท-เท่อ-เบิ้ล = ตรวจสอบได้) — ตรวจสอบย้อนหลังได้ (event ถูกบันทึก)
  • Resilient (เร-ซิล-เลียนท์ = ทนทาน, ฟื้นได้) — ทนทาน (event เก็บถาวร replay ได้)

2. Event vs Command vs Message — แยกให้ออก

สรุปสั้น ๆ ก่อนเข้าเนื้อหา:

คำคำอ่านความหมาย
Eventอี-เวนต์เหตุการณ์ที่เกิดขึ้นแล้ว (อดีต)
Commandคอม-แมนด์คำสั่งให้ใครทำอะไร (จะทำในอนาคต)
Messageเมส-เซจข้อความกลาง ๆ (เป็น event หรือ command ก็ได้)

สามคำนี้ฟังดูคล้ายแต่ความหมายต่างกันและใช้ผิดบ่อย — command คือ "จงทำ X" (สั่งให้ service เจาะจงทำ อาจถูกปฏิเสธ), event คือ "X เกิดขึ้นแล้ว" (ข้อเท็จจริงในอดีต กระจายให้ผู้สนใจ), message เป็นคำกลาง ๆ EDA ยึด event เป็นหลักเพราะมันคือ fact ที่ใครจะ react ก็ได้:

text
Command (คำสั่ง):
- "จงทำ X" (รูปคำสั่ง, อนาคต)
- ส่งไปยัง service เจาะจง
- service อาจปฏิเสธได้
- ตัวอย่าง: "ChargeCard" (จงหักบัตร)

Event (เหตุการณ์):
- "X เกิดขึ้นแล้ว" (อดีตกาล)
- กระจายให้ผู้ที่สนใจ
- เป็นข้อเท็จจริง (เปลี่ยนไม่ได้/immutable)
- ตัวอย่าง: "PaymentCharged" (หักบัตรแล้ว)

Message (ข้อความ):
- คำกลาง ๆ เป็นได้ทั้ง command หรือ event

→ EDA ใช้ event เป็นหลัก (ไม่ใช่ command)


3. Why Event-Driven — ทำไมต้องใช้ event

ทำไมต้องเปลี่ยนจากการเรียก service ต่อกันเป็นลูกโซ่ (sync)? เพราะมันทำให้ coupling แน่น, เกิด cascade failure (ปลายสายล่ม → ทั้งสายบล็อก) และเพิ่ม consumer ใหม่ทีต้องแก้โค้ดต้นทาง EDA แก้ทั้งหมดนี้ด้วยการ "ยิง event แล้วจบ" ให้ใครสนใจก็มา subscribe เอง ลองเทียบสองแบบ:

Problems: Coupling (Order knows all downstream) · Cascade failure · Adding service requires modifying Order · User waits for everything

Benefits: Decoupled · Each service scales independently · Add consumer without changing Order · Async (Order returns fast) · Audit trail


4. Kafka — The Standard

📝 Kafka (อ่าน "คาฟ-ก้า") — ตั้งชื่อตามนักเขียนชาวเช็ก Franz Kafka; LinkedIn สร้างขึ้นในปี 2010 แล้วบริจาคให้ Apache

ก่อนเข้า Kafka — ทำความเข้าใจ Mental Model

หลายคนเริ่ม Kafka แล้วงงเพราะ vocabulary เยอะมาก — ลอง analogy ก่อน:

Analogy: Kafka เหมือน "mailing list" + "ตู้รับจดหมายหลายตู้"

text
Topic = หัวเรื่อง mailing list (เช่น "orders", "payments")
   ↳ Partition = ตู้ mailbox หลายตู้ที่กระจายงานในหัวเรื่องเดียวกัน
       ↳ (ภายในตู้เดียว เรียงตามลำดับเข้ามา = ordering ภายใน partition)
       ↳ Offset = หมายเลขจดหมายในตู้ (เพิ่มขึ้นเรื่อยๆ ไม่ลด)

Producer = นักข่าว เขียนข่าวใส่ topic
Consumer = ผู้สมัครสมาชิก อ่านจาก topic
Consumer Group = "กลุ่มผู้สมัคร" ที่แชร์งานกัน
Broker = ตึก server ที่เก็บข่าวจริงๆ

Key concepts ที่ต่างจาก queue ปกติ:

text
Queue ปกติ (RabbitMQ, SQS):
- Consumer ดึง message → message หาย
- Order = first-in-first-out
- 1 consumer = 1 message

Kafka:
- Consumer "อ่าน" message → message ยังอยู่ (retain ตาม config)
- Order ภายใน partition (ไม่ใช่ topic ทั้งหมด)
- Multiple consumers/groups อ่าน topic เดียวกันได้ — ไม่แย่งกัน
- "Replay" ได้: consumer reset offset → อ่านใหม่ตั้งแต่ต้น

→ Kafka ไม่ใช่ queue — เป็น "log" ที่หลายคน "อ่าน position ของตัวเอง"

โครงสร้างของ Topic

text
Topic "orders" (partitioned by order_id):

  Partition 0 [oldest ─→ newest]
    offset:  0   1   2   3   4   5   ...
    ┌─────┬─────┬─────┬─────┬─────┬─────┐
    │ evt │ evt │ evt │ evt │ evt │ evt │ ...
    └─────┴─────┴─────┴─────┴─────┴─────┘
  
  Partition 1
    offset:  0   1   2   3   ...
    ┌─────┬─────┬─────┬─────┐
    │ evt │ evt │ evt │ evt │ ...
    └─────┴─────┴─────┴─────┘
  
  Partition 2
    ...

Properties สำคัญ:

text
✅ Order ภายใน partition: guarantee (การันตี = รับประกัน) — offset 0 < 1 < 2 ...
❌ Order ข้าม partition: ไม่ guarantee (partition 0[5] อาจหลัง partition 1[3])
✅ Append-only: ไม่ลบ ไม่แก้ — เพิ่มต่อท้ายอย่างเดียว
✅ Immutable: เมื่อ write แล้ว ไม่เปลี่ยน

Consumer Group — แบ่งงานยังไง?

text
Topic "orders" มี 3 partitions:

Consumer Group "billing-service" (3 consumers):
  Consumer 1 ← Partition 0
  Consumer 2 ← Partition 1
  Consumer 3 ← Partition 2
  → ทุก consumer ทำงาน parallel, ทุก event ถูก process

Consumer Group "billing-service" (2 consumers):
  Consumer 1 ← Partition 0, 1  ← ดูแล 2 partitions
  Consumer 2 ← Partition 2
  → ยังคง 1 partition / 1 consumer (Kafka assign แบบ load-balance)

Consumer Group "billing-service" (5 consumers, 3 partitions):
  Consumer 1, 2, 3 ← Partition 0, 1, 2 (each consumer assigned 1)
  Consumer 4, 5 ← IDLE (มากกว่า partition!)
  → ❌ "5 consumers / 3 partitions → 3 ทำงาน 2 ว่าง"
  → partition = max parallelism (upper bound) ของ consumer group นี้

กฎทอง: จำนวน partition = upper bound ของ consumer ที่ active พร้อมกันใน group (ส่วนเกินจะ idle เฉย ๆ — ไม่ใช่ว่า "ต้องเท่ากัน")

⚠️ ดังนั้นเลือกจำนวน partition ตอนสร้าง topic สำคัญมาก — เพิ่ม partition ทีหลังได้แต่จะกระทบ key→partition mapping (event ของ key เดียวกันอาจไปคนละ partition); ลด partition ไม่ได้ ต้องสร้าง topic ใหม่แล้ว migrate

Rule of thumb: ตั้งจำนวน partition ให้ใหญ่กว่า consumer ที่คิดว่าจะมี 2-3 เท่า เผื่อ scale

text
Multiple Consumer Groups (different apps):
  Consumer Group "billing" (independent)
  Consumer Group "analytics" (independent)
  Consumer Group "audit-log" (independent)
  
  → ทุก group อ่าน topic เดียวกัน อย่างอิสระ
  → group ไม่กระทบกัน

Kafka Architecture แบบเต็ม

ศัพท์พื้นฐาน

text
Apache Kafka:
- Distributed event streaming platform
- Originally LinkedIn (2010), now Apache
- High throughput (millions msg/sec)
- Persistent (configurable retention)
- Replay-able (stored in log)
- Partitioned (scale)

สถาปัตยกรรม

text
Topic: named event stream
- Each topic split into N partitions
- Each partition = ordered log

Producer: writes to topic
Consumer: reads from topic
Broker: server hosting topics/partitions
Cluster: multiple brokers
ZooKeeper / KRaft: coordination (ดูหมายเหตุ timeline ข้างล่าง)
  - ZooKeeper (เก่า, แยก process, ZAB protocol) — Kafka 4.0 ลบทิ้งหมดแล้ว
  - KRaft (ใหม่, ใช้ Raft built-in ใน Kafka, ไม่ต้อง deploy ZK แยก)

Consumer Group:
- Multiple consumers share work
- Each partition assigned to 1 consumer in group
- Multiple groups = independent consumers (different apps)

📌 TL;DR สำหรับ basic ในปี 2026: ใช้ KRaft (default ตั้งแต่ Kafka 3.3, 2022; Kafka 4.0 ships ZooKeeper-less by default — ลบ ZK ออกจาก codebase ทั้งหมด); cluster ใหม่ ไม่ต้องห่วง ZK legacy

มือใหม่ทำความเข้าใจสั้น ๆ:

ตัวคืออะไรสถานะ 2026
ZooKeeperexternal coordination service (เคยใช้คู่กับ Kafka)❌ ถูกลบใน Kafka 4.0
KRaftbuilt-in Raft consensus ใน Kafka เอง✅ default, ใช้ใน 4.0+
ZABprotocol ของ ZooKeeper (consensus-style)— เกี่ยวข้องแค่ผู้ดูแล ZK legacy

📅 KRaft vs ZooKeeper timeline (ละเอียดสำหรับคน operate Kafka เอง):

  • Kafka 3.0 (Sep 2021) — KRaft early access, ไม่ recommend production
  • Kafka 3.3 (Oct 2022) — KRaft GA สำหรับ new clusters
  • Kafka 3.5 (Jun 2023) — KRaft production-ready + ZooKeeper deprecated
  • Kafka 3.7 (Feb 2024) — Migration tool (ZK → KRaft) GA
  • Kafka 4.0 (Mar 2025)ships ZooKeeper-less by default; ZK code path ถูกลบ; ทุก cluster ต้อง migrate เป็น KRaft ก่อน upgrade เป็น 4.x

ในปี 2026: new clusters ทุกตัวควรเป็น KRaft; Confluent Cloud และ AWS MSK ได้ทำ KRaft migration เสร็จสมบูรณ์ให้ลูกค้า managed ทั้งหมดแล้วในช่วงปี 2024-2025; Redpanda ไม่เคยใช้ ZK ตั้งแต่แรกอยู่แล้ว

🆕 Kafka 4.0 — feature สำคัญที่ต้องรู้ (Early Access ทั้งคู่):

  • KIP-848 (Next-gen consumer group protocol) — รื้อ rebalance protocol ใหม่ ลด stop-the-world pause ตอน rebalance; ยัง Early Access ใน 4.0 (ยังไม่ default) — จะกระทบ exactly-once setup ในอนาคต
  • KIP-932 (Share groups) — model ใหม่ที่ผสม "queue semantics" (per-message ack) เข้ากับ topic ของ Kafka; เหมาะกับ work distribution ที่ไม่ต้องการ partition ordering; Early Access ใน 4.0

ภาพประกอบ

text
Topic: "orders"
   Partition 0: [event1, event2, event3, ...]
   Partition 1: [eventA, eventB, eventC, ...]
   Partition 2: [eventX, eventY, eventZ, ...]

Consumer Group "billing":
   Consumer 1 ← Partition 0
   Consumer 2 ← Partition 1
   Consumer 3 ← Partition 2

Consumer Group "analytics":
   Consumer 1 ← Partition 0, 1, 2 (1 consumer = all partitions)

Both groups receive every event independently

5. Kafka Producer — ฝั่งส่ง message

เดินทีละบรรทัด (สำหรับมือใหม่):

  • clientId — ชื่อเรียก app ของเรา (โผล่ใน log + metric ของ broker)
  • brokers: ['kafka:9092'] — รายการ "bootstrap broker"; client ใช้ตัวนี้เพื่อค้นหา broker ตัวอื่นใน cluster
  • kafka.producer() — สร้าง producer instance (ค่า default ตามเวอร์ชัน lib)
  • producer.send({ topic, messages }) — ส่งทั้ง batch ไป topic; แต่ละ message มี key (ใช้ตัดสิน partition), value (payload), headers (metadata)
typescript
import { Kafka } from 'kafkajs';

const kafka = new Kafka({
    clientId: 'my-app',         // ชื่อ app — โชว์ใน broker log/metric
    brokers: ['kafka:9092']     // bootstrap broker — client discover broker อื่นต่อ
});

const producer = kafka.producer();
await producer.connect();

// Send
await producer.send({
    topic: 'orders',
    messages: [
        {
            key: orderId,        // partition by this
            value: JSON.stringify({
                orderId,
                userId,
                items,
                total
            }),
            headers: {
                'event-type': 'OrderCreated',
                'event-id': uuid()
            }
        }
    ]
});

await producer.disconnect();

Partition Key — กุญแจกำหนด partition

text
key = orderId
→ All events for same order go to same partition
→ Ordering preserved (per order)
→ Required for state machine reconstruction

Acks (Reliability) — ระดับการรับประกันการเขียน

text
acks: 0
- Producer doesn't wait for ACK
- Fast but lossy

acks: 1
- Wait for leader ACK (default)
- Lose if leader dies before replication

acks: -1 (all)
- Wait for in-sync replicas ACK
- Most durable, slower
- **หมายเหตุ:** `acks=-1` และ `acks="all"` เป็น syntax เดียวกัน (ค่าเหมือนกัน) — บาง config ใช้ "-1" บาง config ใช้ "all"

Production: acks: all + idempotent producer

Idempotent Producer — กันส่งซ้ำ

typescript
const producer = kafka.producer({
    idempotent: true,
    maxInFlightRequests: 5     // Kafka 1.1+ allows up to 5 with idempotence
    // ถ้าตั้ง = 1 → strict ordering แต่ throughput ลด;
    // ถ้าตั้ง = 5 (default ใหม่) → throughput สูงและยัง exactly-once ภายใน partition
});

→ Retry แล้วก็จะไม่เกิด duplicate ใน partition เดียวกัน

📝 หมายเหตุ: Kafka 1.1+ รองรับ idempotent producer ที่ max.in.flight.requests.per.connection ≤ 5 (ไม่จำเป็นต้อง = 1); ตั้ง 1 = trade throughput เพื่อความ strict ของ ordering — ส่วนใหญ่ใช้ค่า default 5 ก็พอ


6. Kafka Consumer — ฝั่งรับ message

ฝั่งรับ event ใช้ consumer ที่ผูกกับ groupId — Kafka จะกระจาย partition ให้ consumer ในกลุ่มเดียวกันช่วยกันอ่าน (load balance) จุดสำคัญที่ต้องเข้าใจคือ "offset commit": ถ้า auto-commit แล้ว process fail อาจเสีย event จึงนิยม manual commit เพื่อ commit เฉพาะเมื่อทำงานสำเร็จจริง:

typescript
const consumer = kafka.consumer({ groupId: 'billing-service' });

await consumer.connect();
await consumer.subscribe({ topic: 'orders', fromBeginning: false });

await consumer.run({
    eachMessage: async ({ topic, partition, message }) => {
        const event = JSON.parse(message.value!.toString());
        await processOrder(event);
        // Auto-commit offset after function returns
    }
});

Manual Offset Management — จัดการ offset เอง

typescript
await consumer.run({
    eachMessage: async ({ topic, partition, message }) => {
        try {
            const event = JSON.parse(message.value!.toString());
            await processOrder(event);
            // Don't commit if process fails
        } catch (err) {
            // Don't commit → will retry on next poll
            throw err;
        }
    },
    autoCommit: false
});

// Manual commit when ready
await consumer.commitOffsets([
    { topic, partition, offset: '12345' }
]);

Consumer Group Rebalance — re-distribute partition ระหว่าง consumer

text
When consumer joins / leaves group:
- Kafka redistributes partitions
- Brief pause during rebalance
- Each partition assigned to new consumer

Be aware:
- In-flight message during rebalance: at-least-once delivery
- Slow rebalance → pause
- Tune session.timeout.ms + heartbeat

7. Delivery Semantics — รับประกันแบบไหน

Analogy เปรียบเทียบ:

แบบเหมือนกับ
At-most-onceส่ง SMS แล้วไม่สนว่าได้ไม่ได้ (อาจหาย แต่ไม่ซ้ำ)
At-least-onceส่งจดหมายลงทะเบียน — รู้ว่าถึง แต่บางทีส่งซ้ำ
Exactly-onceส่งจดหมายแล้วเซ็นรับ + cross-check serial — ถึงแน่ ครั้งเดียวแน่

Message lifecycle (ฝั่ง consumer):

text
[poll message] → [process] → [commit offset]
     ↑                              ↓
     └──────── ถ้า crash ── ก่อน/หลัง commit ทำให้กลายเป็น at-most vs at-least-once

At Most Once — ส่งสูงสุดครั้งเดียว

text
Consumer commit offset "ก่อน" process
- crash ระหว่าง process → ไม่ process ซ้ำ
- message หาย (ครั้งเดียว)
- เร็วแต่เสี่ยงข้อมูลหาย

At Least Once (default) — รับประกันส่งถึง

text
Consumer commit offset "หลัง" process
- crash ระหว่าง process → retry ใหม่
- message อาจถูก process หลายครั้ง
- consumer ต้อง idempotent!

Exactly Once — ส่งครั้งเดียวเป๊ะ

text
Kafka 0.11+:
- Idempotent producer: no duplicate writes within partition
- Transactions: atomic write to multiple partitions
- Read-process-write: atomic consumer + producer

Workflow:
1. Consumer reads from input topic
2. Process
3. Produce to output topic + commit offset (atomic)
   - All succeed or all fail

Required setup (ครบทุกบรรทัด — ขาดอันใดอันหนึ่ง = ไม่ได้ exactly-once):
- enable.idempotence = true (producer) — กัน duplicate write
- transactional.id = "<unique-id>" (producer) — เปิด transaction support
- isolation.level = read_committed (consumer) — ข้าม uncommitted record
- API pattern (producer):
    initTransactions()
    beginTransaction()
    send(...) + sendOffsetsToTransaction(...)
    commitTransaction()   // (หรือ abortTransaction() เมื่อพลาด)

Limitations:
- Only within Kafka (not external systems)
- Slower than at-least-once
- Not "true" exactly-once for cross-system (still need idempotent consumer)
- **Kafka 4.0 KIP-848 (Early Access)** — consumer group protocol ใหม่กระทบวิธีจัดการ EOS rebalance; ในปี 2026 ยังไม่ default แต่ team ที่ใช้ EOS ควรจับตาดูการเปลี่ยน behavior

💡 สรุปสั้น ๆ: Exactly-once ใน Kafka = idempotent producer + transactional consumer (read_committed) + commit offset ใน transaction เดียวกับ output → ครบ pattern ที่เรียกว่า "transactional read-process-write"


8. Replication + Durability — สำเนาและความทนทาน

Kafka ทนทานเพราะแต่ละ partition มีหลายสำเนา (replication factor) กระจายข้าม broker — มี leader รับอ่าน/เขียน และ follower คอย sync พร้อมขึ้นแทน คอนเซ็ปต์สำคัญคือ ISR (in-sync replica) + acks=all + min.insync.replicas ที่รับประกันว่า write จะไม่หายแม้ broker ล่ม:

text
Topic replication factor (RF) = จำนวนสำเนา:
- 1: สำเนาเดียว ไม่มี redundancy
- 3: 3 สำเนา (ค่าปกติใน production)

ต่อ partition:
- 1 leader + 2 follower (เมื่อ RF=3)
- Leader: รับอ่าน + เขียน
- Followers: คอย replicate พร้อมขึ้นแทน

In-Sync Replica (ISR) = follower ที่ทันต่อ leader (lag ต่ำ); broker ใช้ตัวนี้ตัดสินว่า write ครบไหม:
- follower ต้อง catch up (ตามทัน) ถึงจะนับเป็น ISR
- Min ISR = ต้องมี ISR กี่ตัวก่อนจะยอมรับ write
(ดู ISR ใน glossary บท 11)

acks=all + min.insync.replicas=2 (+ unclean.leader.election.enable=false):
- write จะสำเร็จต่อเมื่อ 2 ใน 3 replica มีข้อมูลแล้ว
- รอด broker ล่ม 1 ตัวได้โดยข้อมูลไม่หาย
- **สำคัญ:** ต้องตั้ง `unclean.leader.election.enable=false` ด้วย — ไม่งั้น broker ที่ตามไม่ทันอาจถูกเลือกเป็น leader → data loss

9. Kafka Use Cases — สถานการณ์ที่นิยมใช้

Kafka ไม่ได้ใช้แค่ส่ง message ระหว่าง service — มันเป็น distributed log ที่ใช้ได้หลายงาน ตั้งแต่ event sourcing, log aggregation, stream processing, real-time analytics ไปจนถึง CDC (จับการเปลี่ยนแปลงใน DB) รายการนี้ช่วยให้เห็นว่าเครื่องมือเดียวครอบคลุมได้กว้างแค่ไหน:

text
1. Async messaging (decouple services)
2. Event sourcing (event log)
3. Log aggregation
4. Stream processing
5. Real-time analytics
6. Data integration (move between systems)
7. Audit trail
8. CDC (Change Data Capture) — replicate DB changes

10. Stream Processing — ประมวลผล event สด ๆ

text
Process events as they flow (not batch)

Operations:
- Filter
- Map / transform
- Aggregate (count, sum, avg over window)
- Join (combine streams)
- Enrich (lookup)

Tools:
- Kafka Streams (Java/Kotlin library)
- Apache Flink ⭐ (Java, most powerful)
- Apache Spark Streaming
- ksqlDB (SQL-like)
- AWS Kinesis Analytics

ตัวอย่าง — Real-time Order Count

typescript
// Kafka Streams (Java/Kotlin pseudo)
KStream<String, Order> orders = builder.stream("orders");

KTable<Windowed<String>, Long> ordersPerMinute = orders
    .groupBy((key, order) -> order.getRegion())
    .windowedBy(TimeWindows.ofSizeWithNoGrace(Duration.ofMinutes(1)))  // Kafka Streams 3.0+ API (TimeWindows.of() ถูก deprecate)
    .count();

ordersPerMinute.toStream().to("orders-per-minute");

→ Stream ของ order → aggregate ต่อ 1 นาที ต่อ region → ส่งออกไป topic อื่นต่อ

Windowing — แบ่ง stream เป็นช่วงเวลา

text
Tumbling window: fixed size, non-overlapping
- 1-min windows: 0-1, 1-2, 2-3 min

Hopping window: fixed size, overlapping
- 5-min window every 1 min

Sliding window: variable
- "Last 5 min from now"

Session window: defined by gap
- Group user activity, gap = end of session

11. Event Sourcing — ลงลึกอีกชั้น

text
Don't store current state — store events

Bank account:
- AccountOpened(holder: Anna, initial: 0)
- Deposit($100)
- Deposit($50)
- Withdraw($30)
- Deposit($200)

Current state = replay events:
- balance = 0 + 100 + 50 - 30 + 200 = $320

ข้อดี

text
✅ Audit trail (regulatory compliance)
✅ Time travel ("What was balance on Jan 1?")
✅ Easy to add projection (read model)
✅ Reactive (events drive everything)
✅ Debugging (replay events to see issue)

ความท้าทาย

text
❌ Schema evolution (events from years ago)
❌ Performance (replay 1B events = slow)
❌ Snapshot needed for hot accounts
❌ Mental model shift

Snapshot — ภาพรวมสำหรับ aggregate ที่ event เยอะ

text
For long-lived aggregates:
- Snapshot every N events
- Read: load snapshot + replay events since

[Snapshot at event 1000]
[Event 1001]
[Event 1002]
...

Load: snapshot + events 1001+

เครื่องมือที่นิยม

text
- EventStoreDB (purpose-built)
- Kafka (used as event store)
- Marten (Postgres-based ES + CQRS)
- AxonServer
- Postgres with custom table

12. Event Schema Evolution — เมื่อ schema ต้องเปลี่ยน

event ถูกเก็บไว้นาน ปีหน้า schema ก็ต้องเปลี่ยน (เพิ่ม/ลบ/แก้ field) แต่จะแก้ยังไงไม่ให้ consumer เก่าพัง? ส่วนนี้สอนกลยุทธ์ backward/forward compatibility, versioning และ upcaster พร้อม Schema Registry ที่เป็นที่เก็บ schema กลางช่วยจับ breaking change ตั้งแต่ตอน deploy:

text
Years pass, event format needs changes
- Add field
- Remove field
- Change type

Strategies:

1. Backward compatibility
   - New consumers can read old events
   - Add field with default value
   - Don't remove (deprecate instead)

2. Forward compatibility
   - Old consumers can read new events
   - Ignore unknown fields

3. Versioning
   - eventType: "OrderCreated.v2"
   - Different schema per version
   - Migration tools

4. Upcaster
   - Read old event format
   - Transform to new format
   - Consumer always sees new format

Schema Registry — ที่เก็บ schema กลาง

text
Confluent Schema Registry:
- Central place for schemas
- Avro / Protobuf / JSON Schema
- Producer registers, consumer validates
- Track compatibility

Benefits:
- Catch breaking changes at deploy
- Smaller messages (binary Avro)
- Clear schema documentation

13. CQRS — ลงลึกอีกชั้น

CQRS (Command Query Responsibility Segregation) แยกฝั่งเขียน (command) ออกจากฝั่งอ่าน (query) — ฝั่งเขียนรับคำสั่ง validate แล้วยิง event, ฝั่งอ่าน subscribe event ไปสร้าง "read model" ที่ปรับแต่งเฉพาะ query แต่ละแบบ (Postgres, Elasticsearch, Redis) ทำให้อ่านและเขียน scale แยกกันและ optimize ได้อิสระ:

text
Command + Query separated:

Command side:
- Receives command (e.g., "PlaceOrder")
- Validates
- Writes to event store
- Emits events

Query side:
- Subscribes to events
- Builds read models (denormalized views)
- Optimized for queries

Read model:
- Postgres view
- Elasticsearch index
- Cache (Redis)
- Different per query pattern

ตัวอย่าง — ระบบ E-commerce

text
Write side:
- OrderService receives "PlaceOrder" command
- Validates inventory + payment
- Writes "OrderPlaced" event

Read models:
1. Order list (Postgres) — for user dashboard
   - Subscribe to OrderPlaced, OrderShipped, OrderDelivered
   - Update row in orders table

2. Search index (Elasticsearch) — for admin search
   - Index full text + filters
   - Update on each event

3. Analytics aggregate (ClickHouse)
   - Roll up by date / region / category
   - For business dashboard

4. User feed (Redis)
   - "Your recent orders" cache
   - Top 10 most recent per user

13.5 Event Sourcing — Production Concerns

⚠️ (ขั้นต่อยอด · production team) — basic ที่เพิ่งเรียน Event Sourcing concept ข้าม section นี้ก่อนได้; กลับมาอ่านก่อน deploy ES system จริงใน production

Event Sourcing ดูสวยใน tutorial แต่ production มี 3 ปัญหาที่ต้องวางแผนตั้งแต่ day 1:

Concern 1: Snapshot Strategy — replay ไม่ทันเมื่อ events เยอะ

text
ปัญหา:
- User มี 100,000 events ตลอด 5 ปี
- Rebuild current state = replay 100,000 events → 30 วินาที
- Cold start service → ทุก aggregate ต้อง replay → slow

ทางแก้ — Snapshot:
- ทุก N events (e.g., N=100) → save aggregate state
- Replay = load latest snapshot + apply events after snapshot
- Reduce replay 100x

Implementation:
{
  aggregate_id: "user-123",
  snapshot_version: 1000,
  state: {...},  // full state at version 1000
  created_at: ...
}

Rebuild:
1. Load latest snapshot (version 1000)
2. Load events where version > 1000
3. Apply events → current state

Snapshot frequency trade-off:

  • More frequent → less replay, more storage
  • Less frequent → more replay, less storage
  • Common: every 100-1000 events, or every 1 hour

Concern 2: Schema Evolution — events เก่าไม่ตรงกับ code ใหม่

text
ปี 1: เก็บ event {type: "OrderPlaced", v1: {orderId, items, total}}
ปี 3: code เปลี่ยน → schema คือ {orderId, items, total, currency, taxAmount}

แต่ events เก่า 1B records ใน DB ยังเป็น v1!

Strategies:
1. Backward compatibility ใน code
   - Old event without currency → default "USD"
   - Calculate taxAmount from total

2. Upcasting
   - Read v1 event → transform to v2 in code
   - "Upcast" function ต่อ event type
   - Single source of truth = current version

3. Event versioning
   - {type: "OrderPlaced", version: 2, ...}
   - Handler routes by version

4. Avro Schema Registry (Confluent)
   - Centralized schema versioning
   - Backward/forward compatibility rules enforced
   - Producer ส่ง schema id, consumer fetches schema from registry

⚠️ Critical: ห้าม "edit events เก่า" — events เป็น immutable facts; ต้อง handle compatibility ใน code

Concern 3: Replay Performance — projection rebuild

text
สถานการณ์:
- เพิ่ม projection ใหม่ (เช่น new dashboard)
- ต้อง replay ทุก event ตั้งแต่ต้น → 1B events → 12 ชั่วโมง

Strategies:
1. Parallel replay
   - Split events by aggregate_id ranges
   - N workers replay parallel
   - Reduce 12h → 1h

2. Bulk insert mode
   - Disable indexes
   - Insert batch
   - Build indexes after

3. Catchup-then-live pattern
   - Replay historical (batch)
   - Switch to live stream (Kafka)
   - Atomic switchover (ใช้ offset/timestamp)

4. Event Sourcing DB optimizations
   - EventStoreDB: stream-based read, fast replay
   - Marten (Postgres): jsonb + projection rebuilds

💡 Production checklist: snapshot + upcasting + parallel replay = ต้องมีทั้ง 3 ก่อน scale event-sourced system to production


14. Outbox Pattern — ทบทวนจากบทที่ 3

ทบทวน outbox pattern สั้น ๆ ในบริบท EDA — ปัญหา dual-write (เขียน DB สำเร็จแต่ publish Kafka ล้มเหลว → state เพี้ยน) แก้ด้วยการเขียน event ลงตาราง outbox ใน transaction เดียวกับ business data แล้วให้ background publisher ส่งไป Kafka ทีหลัง รับประกันว่า DB กับ event ตรงกันเสมอ:

text
Atomic: DB write + event publish

INSERT INTO orders ...
INSERT INTO outbox (event, payload) ...
COMMIT (atomic)

Background publisher:
- Read outbox
- Publish to Kafka
- Mark published

Without outbox: dual-write problem
- DB write succeeds
- Kafka write fails
- State drift

⚠️ Outbox ≠ exactly-once ฝั่ง consumer: outbox รับประกันแค่ "DB กับ event ตรงกัน" แต่ background publisher อาจส่ง event ซ้ำ (crash หลัง publish ก่อน mark) → consumer ต้อง idempotent เสมอ (เช็ค event_id เพื่อกัน duplicate)

🔗 เกี่ยวข้อง CDC (Change Data Capture) — Debezium pattern: อีกทางเลือกหนึ่งคือใช้ Debezium (เดอ-เบ-เซียม) อ่าน DB transaction log (WAL/binlog) แล้ว publish ไป Kafka — ไม่ต้องเขียนตาราง outbox เอง ดูส่วน "Kafka Use Cases" สำหรับ CDC ด้านล่าง


15. Saga ใน EDA

Choreography Pattern — แต่ละ service ตัดสินใจเอง

Each service listens + emits events. No central coordinator.

Orchestration Pattern — มี coordinator ส่วนกลาง

Coordinator service orchestrates: knows entire flow, calls each service in order, handles compensation.

→ EDA เข้ากันกับ choreography โดยธรรมชาติ แต่ orchestration ติดตาม flow ได้ง่ายกว่า


16. Stream Joining — รวม stream เข้าด้วยกัน

บางครั้งต้องรวมข้อมูลจากหลาย stream เข้าด้วยกัน — เช่นเอา order มา enrich ด้วยข้อมูล user ก่อน process การ join stream มีหลายแบบ (stream-stream แบบ window, stream-table, table-table) และมักคู่กับ materialized view ที่เก็บ "สถานะปัจจุบัน" จาก event stream ไว้ให้ join ได้เร็ว:

text
Join two streams:

Stream 1: orders
Stream 2: user profiles

For each order, enrich with user info:

orders.join(users, (o, u) => ({ ...o, user: u }))
       .to('enriched-orders')

Types:
- Stream-stream join (windowed)
- Stream-table join (table = current state of another stream)
- Table-table join

Materialized View Pattern — สร้าง "ตาราง" จาก stream

text
Maintain "table" view from event stream

Event: "ProductPriceChanged"
- Update materialized view (current price)

Event: "ProductCreated"
- Insert into view

Stream of orders → join with materialized view to enrich

17. ตัวอย่างจริง — Twitter-like Feed

มาดูทุก concept ในบทประกอบกันเป็นระบบจริง — timeline แบบ Twitter ที่ใช้ event (TweetPosted, UserFollowed), topic ที่ partition ตาม user, stream processor ที่ fan-out tweet ไปยัง follower และ read model ใน Redis ตัวอย่างนี้แสดงว่า EDA + CQRS + materialized view ทำงานร่วมกันยังไงในสเกลใหญ่:

text
Events:
- "TweetPosted" — user posted
- "UserFollowed" — A followed B
- "UserUnfollowed" — A unfollowed B
- "TweetDeleted" — user deleted

Topics:
- tweets (partitioned by user_id)
- follows (partitioned by follower_id)
- timeline-updates (per user, partitioned by user_id)

Stream Processor:
- Subscribe to tweets + follows
- On TweetPosted by user X:
   - Look up X's followers (from materialized view)
   - For each follower: emit timeline-update event
- On UserFollowed (A follows B):
   - Add B's recent tweets to A's timeline

Read Side:
- User opens timeline
- Read from Redis: user's feed (precomputed list)
- Fast (~1 ms)

→ Fanout แบบ async + timeline ที่ eventually consistent


18. Topic Design Best Practices — แนวทางออกแบบ topic

การออกแบบ topic ดี ๆ ตั้งแต่แรกสำคัญมากเพราะแก้ทีหลังยาก (โดยเฉพาะจำนวน partition ที่ลดไม่ได้) — ส่วนนี้รวมแนวทาง: ตั้งชื่อให้เป็นระบบ, เลือกจำนวน partition ตาม parallelism ที่ต้องการ (เผื่อโต 2-3 เท่า), ตั้ง retention/compression และ replication ให้เหมาะ:

text
1. Topic naming:
   <domain>.<entity>.<event>
   - order.payment.charged
   - user.profile.updated

2. Partition count:
   - More partitions = more parallelism
   - But: more overhead per partition
   - Rule of thumb: 1 partition per consumer thread you'll need
   - Plan for 2-3x growth (can't reduce easily)

3. Retention:
   - Default 7 days
   - Event sourcing: forever or compact
   - Logs: 1-30 days

4. Compression:
   - LZ4 (fast) or zstd (best ratio)
   - Significant savings (60-80% reduction)

5. Replication:
   - Production: replication factor 3
   - Min ISR 2 + acks all = no data loss with 1 broker down

19. Kafka Alternatives — ทางเลือกอื่นนอกจาก Kafka

text
Apache Pulsar:
- Multi-tenancy native
- Geo-replication built-in
- Different architecture (3 components: Brokers + BookKeeper + metadata store)
  → ops ซับซ้อนกว่า Kafka เพราะต้องดูแล BookKeeper + ZK-like metadata store เพิ่ม
- Used by: Yahoo, Tencent

Redpanda:
- Kafka API compatible
- Written in C++ (faster, less resource)
- No ZooKeeper needed (ตั้งแต่ day 1)
- Used by: many modern projects

NATS JetStream:
- Lightweight
- Multi-language
- Good for IoT, edge
- ตั้งแต่ 2.10 รองรับ Kafka-compatible consumer mode (ลด lock-in)

AWS Kinesis:
- Managed AWS
- Similar to Kafka
- 24h retention default, **configurable up to 365 days** (เปลี่ยนจาก 7 วันเดิมในปี 2020)
- **On-Demand mode (2022)** — auto-scale ไม่ต้องจัดการ shard เอง

GCP Pub/Sub:
- Fully managed
- At-least-once
- Pub/Sub (standard) = no partitioning (different model)
- **Pub/Sub Lite (2020+)** — IS partitioned, ใกล้เคียง Kafka มาก เลือกได้ตาม workload

EventHub (Azure):
- Kafka-compatible API

→ บริษัทส่วนใหญ่เลือก Kafka หรือ managed cloud ที่เทียบเคียง


20. Monitoring Kafka — เฝ้าระวังที่ต้องทำ

จะรัน Kafka ใน production ต้องมองเห็นสุขภาพของมัน — metric ที่ต้องจับตาแบ่งตามฝั่ง: producer (error/latency), broker (disk, under-replicated partition), consumer (lag คือตัวสำคัญที่สุด) และ topic ตัวเลขเหล่านี้บอกล่วงหน้าว่าระบบกำลังจะมีปัญหาก่อนที่ user จะเจอ:

text
Key metrics:
- Producer:
   - Send rate
   - Error rate
   - Latency (p99)
   - Retry rate

- Broker:
   - Bytes in/out
   - Disk usage per partition
   - Under-replicated partitions
   - Active controller (1 should be active)

- Consumer:
   - Lag (current offset vs latest)
   - Consumption rate
   - Rebalance rate

- Topic:
   - Message rate per topic
   - Partition leader balance

Consumer Lag — ระยะห่างระหว่าง offset

text
Lag = messages waiting to be processed

If lag growing:
- Consumer not keeping up
- Need: more consumers, faster processing, or capacity

Tools:
- Kafka UI (Provectus) — open source
- Confluent Control Center — paid
- Burrow (LinkedIn) — lag monitor
- Kafka Manager (Yahoo)

21. Multi-DC Replication — replication ข้าม data center

องค์กรใหญ่มักต้องรัน Kafka หลาย data center — เพื่อ DR, มีตัวอยู่ใกล้ผู้ใช้แต่ละภูมิภาค หรือ migration เครื่องมือหลักคือ MirrorMaker 2 ที่ mirror topic ข้าม cluster (active-passive หรือ active-active) แต่ต้องระวังเรื่อง replication lag และ offset ที่เป็นอิสระต่อ cluster:

text
MirrorMaker 2:
- Mirror topics from cluster A to cluster B
- Active-passive or active-active
- Tools handle failover

Use:
- DR (disaster recovery)
- Global presence (regional clusters)
- Migration

Trade-offs:
- Replication lag across regions
- Offsets independent per cluster
- Failover requires coordination

22. Dead Letter Queue (DLQ) — "ที่เก็บ message ที่ process ไม่ได้"

ปัญหา — message ที่ process ไม่สำเร็จ ทำยังไง?

text
Consumer ได้ message → ลองทำ → fail
→ Retry → ก็ fail อีก
→ Retry → fail อีก
→ ทำอะไรต่อ?

ตัวเลือก:
1. Skip message → ❌ ข้อมูลสูญหาย
2. Retry forever → ❌ block partition, lag เพิ่ม
3. ส่งไป "Dead Letter Queue" → ✅ best practice

Dead Letter Queue คืออะไร

DLQ = Topic แยกที่เก็บ "message ที่ process ไม่ได้" ส่งไปด้วย metadata: error reason, retry count, original topic, timestamp

DLQ ไม่ block main topic

Pattern 1: Simple DLQ — ส่งตรงเข้า DLQ เมื่อ retry ครบ

typescript
async function processOrder(message: KafkaMessage) {
    const MAX_RETRIES = 3;
    let attempt = 0;
    
    while (attempt < MAX_RETRIES) {
        try {
            await businessLogic(message);
            return; // success
        } catch (err) {
            attempt++;
            if (attempt < MAX_RETRIES) {
                // exponential backoff + full jitter (กัน thundering herd ของหลาย consumer)
                const base = 2 ** attempt * 1000;
                await sleep(base * (0.5 + Math.random() * 0.5));
                continue;
            }
            
            // Send to DLQ
            await producer.send({
                topic: 'orders.DLQ',
                messages: [{
                    key: message.key,
                    value: message.value,
                    headers: {
                        'original-topic': 'orders',
                        'error': err.message,
                        'failed-at': new Date().toISOString(),
                        'retry-count': String(MAX_RETRIES),
                    }
                }]
            });
        }
    }
}

Pattern 2: Retry Topics (Tiered) — retry หลายขั้นก่อนเข้า DLQ

แนวคิดของ Uber + Confluent:

ดี: ไม่ block main topic, exponential backoff อัตโนมัติ

Pattern 3: Poison Pill — แยกประเภท error ก่อน retry

text
Error types:
1. Transient (retry-able):
   - DB timeout
   - 5xx จาก downstream service
   - Network glitch
   → Retry

2. Permanent (skip-able):
   - Schema error (msg format ผิด)
   - Business rule violation
   - 4xx จาก downstream
   → DLQ ทันที (อย่า retry)

if (error instanceof PermanentError) {
    sendToDLQ(message, error);
} else {
    await retry(message);
}

DLQ Monitoring + Alerting — เฝ้าระวัง DLQ

text
ต้อง monitor:
- DLQ size (เพิ่มเร็วไหม?)
- Age ของ oldest message ใน DLQ (เก่ามาก = ปัญหาเรื้อรัง)
- Error type distribution (ส่วนใหญ่ปัญหาเดียวกัน?)

Alert:
- DLQ size > 100 → page on-call
- DLQ growth rate > 10/min → critical alert

Tools:
- Kafka UI (Provectus) — เห็น DLQ ได้
- Custom dashboard ที่ subscribe DLQ topic + count

Reprocessing จาก DLQ — ส่งกลับเข้า main topic

text
Engineer fix bug → ต้อง reprocess message ที่อยู่ใน DLQ

Workflow:
1. Kafka consumer อ่านจาก DLQ → publish กลับไป main topic
2. Main consumer ทำงานต่อ

Tool:
- ในงาน manual: kafkacat / kcat ดึง + ส่งใหม่
- Production: dedicated "DLQ replayer" service ที่มี UI ให้ admin trigger

23. Backpressure — "เมื่อ producer เร็วกว่า consumer"

ปัญหา

text
Producer ส่ง events 10,000/sec
Consumer process ได้ 1,000/sec
→ Lag เพิ่มขึ้น 9,000 events/sec
→ Kafka topic บวมเรื่อยๆ
→ Memory/disk เต็ม
→ ระบบล่ม

Backpressure คืออะไร — แนวคิด

Backpressure = "การส่งสัญญาณจาก downstream → upstream ให้ ช้าลง/หยุด"

Analogy: เหมือนน้ำในท่อ — ถ้าปลายท่อตัน → ต้องบีบน้ำต้นทางให้ช้าลง ไม่งั้นท่อแตก

Backpressure ใน Kafka — กลยุทธ์ที่ใช้

Kafka ไม่มี backpressure อัตโนมัติแบบ TCP — ต้อง engineer เอง:

text
Strategies:
1. Limit producer rate
   - Application-level rate limit
   - Token bucket per producer
   
2. Consumer batch + pause
   - Read N messages, process, then read more
   - Don't poll faster than process speed

3. Async + buffer + drop oldest
   - Producer buffers in memory
   - If buffer full → drop oldest (lossy backpressure)
   
4. Reactive streams
   - Project Reactor / RxJava
   - Built-in backpressure protocol

ตัวอย่าง 1 — Consumer-driven Pacing

typescript
// แทนที่จะ poll เร็วๆ — process ก่อน poll ใหม่
const consumer = kafka.consumer({ 
    groupId: 'orders-processor',
    maxBytesPerPartition: 1024 * 1024,  // 1MB per poll (จำกัด)
});

await consumer.run({
    autoCommit: false,
    eachBatch: async ({ batch, resolveOffset, heartbeat }) => {
        for (const message of batch.messages) {
            await processSlowly(message);  // ใช้เวลา
            resolveOffset(message.offset);
            await heartbeat();  // บอก Kafka ว่ายังทำงานอยู่
        }
    }
});

ตัวอย่าง 2 — Reactive Streams (Java)

java
// Project Reactor
KafkaReceiver.create(receiverOptions)
    .receive()
    .flatMap(record -> processAsync(record), CONCURRENCY)  // limit parallel
    .onBackpressureBuffer(MAX_BUFFER)
    .doOnError(err -> log.error("Failed", err))
    .subscribe();

Backpressure ในระบบ end-to-end — ส่งแรงดันย้อนทั้ง chain

Each link can apply backpressure:

  • Client: respect 429 (Too Many Requests)
  • Gateway: rate limit per client
  • Service A: connection pool limit
  • Kafka: producer.send() throws if buffer full
  • Consumer: limit poll rate

End-to-end principle: pressure ต้องไหลย้อนกลับ

Drop Strategies — กลยุทธ์เมื่อ backpressure ไม่พอ

text
ถ้า buffer เต็ม + ลด rate ต้นทางไม่ได้:

1. Drop oldest: ใช้กับ "real-time data" (เก่า = หายแล้ว เช่น stock price)
2. Drop newest: ใช้กับ "completeness matter" (เก็บของเก่าไว้)
3. Drop random: ใช้กับ analytics (sampling)
4. Reject + 503: ใช้กับ "must process all" (let client retry)

Choose ตาม business semantics

24. Schema Registry — ทำให้ event มี contract

ปัญหา — Event Schema เปลี่ยน

text
Day 1: Producer ส่ง {name: "Anna", age: 30}
Day 30: Producer เพิ่ม field {name: "Anna", age: 30, email: "anna@x"}
Day 60: Producer เปลี่ยน schema {fullName: "Anna", age: 30, email: "anna@x"}

Consumers ที่ deploy:
- Consumer A (เก่า) → ยังคาดว่า {name, age} → ❌ schema ผิด → crash
- Consumer B (ใหม่) → คาดว่า {fullName, age, email} → ✅

Schema Registry — ทางออก

Compatibility Rules — กติการะหว่าง version

text
Backward Compatibility:
   New consumer สามารถอ่าน old data ได้
   วิธี: เพิ่ม field with default value, ห้ามลบ required field

Forward Compatibility:
   Old consumer สามารถอ่าน new data ได้
   วิธี: ignore unknown fields

Full Compatibility:
   ทั้ง backward + forward — ปลอดภัยที่สุด

ตัวอย่าง Avro Schema — schema ของ Order

json
{
  "type": "record",
  "name": "Order",
  "namespace": "com.example",
  "fields": [
    {"name": "orderId", "type": "string"},
    {"name": "userId", "type": "string"},
    {"name": "total", "type": "double"},
    {"name": "email", "type": ["null", "string"], "default": null}  // backward compat
  ]
}

Schema Registry Tools — ตัวเลือกในตลาด

text
Confluent Schema Registry — มาตรฐาน (ฟรี + commercial)
AWS Glue Schema Registry — AWS managed
Apicurio Registry — open source alternative

Best Practices — แนวทางใช้ Schema Registry

text
✅ Use Avro/Protobuf (binary + schema) แทน JSON ใน production
✅ Set compatibility mode = FULL (เปลี่ยนยากกว่าแต่ปลอดภัย)
✅ Version schema ชัดเจน (v1, v2, v3)
✅ Test schema evolution ก่อน deploy
✅ Document breaking changes ใน changelog

❌ ไม่ทำ schema evolution แบบ "rename field" — break ทั้ง backward + forward
❌ ไม่ deploy producer ก่อน consumer ถ้าเป็น breaking change

25. Event-Driven Anti-Patterns — กับดักที่ต้องเลี่ยง

EDA มีกับดักเฉพาะตัวที่ทำให้เสียประโยชน์ของมัน — ทำ event ให้เป็น command แฝง (บอกให้ทำแทนที่จะเล่าว่าเกิดอะไร), ใช้ event แบบ sync (รอ "response event"), ลืม idempotency หรือ over-engineer (แอปเล็กแต่ยัด Kafka + event sourcing) รู้กับดักพวกนี้ไว้จะใช้ EDA ได้ถูกทาง:

text
1. Tight coupling in events
   - "Process this in this exact way"
   - Event = command in disguise
   → Events should be facts, consumers decide what to do

2. Synchronous communication via events
   - Wait for "response event"
   - Defeats async purpose
   → Use HTTP for sync, events for async

3. Long event chain
   - 10 services in sequence via events
   - Hard to track, debug
   → Limit chain length, use saga orchestrator for complex

4. Events as database
   - Query events to find current state
   - Slow + complex
   → Use projections / read models for queries

5. Forget idempotency
   - At-least-once delivery → duplicate
   - Without idempotency → bug
   → Always idempotent consumers

6. No schema versioning
   - Add field, break old consumers
   → Schema registry + compatibility rules

7. Ignore ordering issues
   - Cross-partition: no order
   - Use partition key wisely

8. Over-engineer
   - Small app + Kafka cluster + event sourcing
   - Way too much
   → Match complexity to need

26. EDA Best Practices — แนวทางที่ควรทำ

ตรงข้ามกับ anti-pattern ด้านบน นี่คือแนวทางที่ควรทำเพื่อให้ EDA ทำงานได้ดีในระยะยาว — ออกแบบ event เป็น fact (อดีตกาล), แนบ event ID + timestamp + schema version, ใช้ partition key คุมลำดับ, ทำ consumer ให้ idempotent และ monitor lag เช็กลิสต์นี้คือพื้นฐานของระบบ event-driven ที่ดูแลได้:

text
✅ Events are facts (past tense): "OrderPlaced"
✅ Include event ID for idempotency
✅ Include timestamps
✅ Include schema version
✅ Use partition key for ordering
✅ Idempotent consumers
✅ Monitor lag
✅ Document event contracts
✅ Use schema registry
✅ Test with production-like data
✅ Plan for replay

27. Migration: Monolith → EDA — ย้ายจาก monolith ไป event-driven

ย้ายจาก monolith มาเป็น event-driven ไม่ควรทำแบบ big-bang (เขียนใหม่ทั้งหมดทีเดียว) เพราะเสี่ยงสูง — ใช้ "Strangler Fig" ที่ค่อย ๆ เพิ่ม Kafka ข้าง ๆ ของเดิม, เริ่มยิง event จาก monolith, ให้ feature ใหม่ consume event แล้วค่อย ๆ แยก service ออกทีละชิ้นจนของเดิมถูกแทนที่:

text
Strangler Fig pattern:

1. Add Kafka alongside existing app
2. Identify event candidates (state changes)
3. Emit events from monolith (keep existing functionality)
4. New features: consume events instead of direct call
5. Gradually extract services
6. Eventually: events become primary integration

→ ทำแบบทีละขั้น (iterative) ไม่ใช่ big-bang

28. ตัวอย่างจริง — Order Processing

[Saga Coordinator] subscribes to all events — tracks order status:

  • PaymentSucceeded + InventoryReserved → emit "OrderConfirmed"
  • PaymentFailed → compensate, emit "OrderFailed"
  • InventoryShort + PaymentSucceeded → refund + emit "OrderFailed"

[Order Service] subscribes to "OrderConfirmed" / "OrderFailed" → update order.status

→ แต่ละขั้นตอนอิสระ retry ได้ และ trace ได้


29. Testing EDA — ทดสอบระบบ event-driven

ระบบ event-driven ทดสอบยากกว่าโค้ด sync เพราะมีหลายชิ้นส่วนทำงานแบบ async — ต้องทดสอบหลายระดับ: unit (command handler), integration (TestContainers รัน Kafka จริง), contract test (producer/consumer ตกลง schema กัน) และ chaos (kill broker/consumer ดูว่า recover ได้ไหม) ส่วนนี้สรุปแต่ละระดับ:

text
Unit test:
- Test command handlers in isolation
- Mock event store

Integration test:
- TestContainers (Postgres + Kafka)
- Test producer → consumer end-to-end

Contract test:
- Pact / Spring Cloud Contract
- Producer + consumer agree on event schema

Chaos:
- Kill broker → consumer reconnects
- Kill consumer → other in group picks up
- Network partition → eventual recovery

30. Cost Considerations — ค่าใช้จ่ายที่ต้องคำนวณ

Kafka cluster ไม่ฟรี — มีค่า broker, storage (event ถูกเก็บไว้) และ network (replication + การอ่านของ consumer) สำหรับ startup ที่ scale ยังไม่ถึง การลงทุน Kafka เต็มรูปแบบอาจเกินจำเป็น เริ่มจาก Postgres + outbox + cron ก่อนแล้วค่อยย้ายเมื่อโตจริงจะคุ้มกว่า:

text
Kafka clusters cost:
- Brokers (replication × N nodes)
- Storage (events stored)
- Network (replication + consumer reads)

Managed Kafka:
- AWS MSK
- Confluent Cloud
- Aiven
- Cost: $$$$ for production cluster

Alternative for small scale:
- Redis Streams (lightweight, in-memory)
- NATS (small + fast)
- RabbitMQ (queue, not log)

For startups:
- Start with Postgres + outbox + cron publisher
- Migrate to Kafka when scale demands

31. Checkpoint — แบบฝึกหัดท้ายบท

ลงมือทำเพื่อให้ความรู้ทั้งบทติดตัว — แบบฝึกหัดนี้ไล่จากรัน Kafka ในเครื่อง, สร้าง producer/consumer, implement outbox + event sourcing ไปจนถึง stream processing ทำครบจะเข้าใจ EDA แบบใช้งานได้จริง:

🛠️ Checkpoint 5.1 — รัน Kafka ในเครื่อง
Docker compose ที่มี:

  • Zookeeper / Kraft
  • Kafka broker
  • Kafka UI (Provectus)

ลองส่ง + consume event

🛠️ Checkpoint 5.2 — แอป Producer/Consumer
สร้าง:

  • Producer ส่ง order event
  • Consumer 1 (billing): หักเงิน + log
  • Consumer 2 (email): ส่งอีเมล
  • ตรวจสอบ: ทั้งคู่ได้รับ event เดียวกัน

🛠️ Checkpoint 5.3 — Outbox Pattern
Spring Boot:

  • endpoint สร้าง order
  • บันทึก DB + outbox ใน transaction เดียว
  • background publisher → Kafka
  • ทดสอบ: kill Kafka แล้ว restart → event ยังถูก publish

🛠️ Checkpoint 5.4 — Event Sourcing
สร้าง Account aggregate:

  • event: opened, deposited, withdrew
  • บันทึก event ลง Postgres
  • สร้าง state ใหม่ตอนอ่าน
  • เพิ่ม snapshot ทุก 100 event

🛠️ Checkpoint 5.5 — Stream Processing
ใช้ Kafka Streams หรือ ksqlDB:

  • topic: page-views (user_id, page, timestamp)
  • output: top 10 หน้า ทุก 1 นาที
  • ตรวจสอบว่าอัปเดต real-time

32. สรุปบท

ทบทวนแก่นของบทนี้ทั้งหมด — ตั้งแต่แนวคิด EDA, Kafka (partition/consumer group/delivery semantics), outbox, event sourcing, CQRS, stream processing ไปจนถึง schema evolution และ anti-pattern เช็กลิสต์นี้คือเครื่องมือสำหรับสร้างระบบ event-driven ที่ scale ได้:

EDA = service ยิง event + react ต่อ event (loose coupling)
Event vs Command: event = อดีตกาล (fact), command = คำสั่ง
Kafka = event log มาตรฐานของวงการ
Partition key รักษาลำดับต่อ key
Consumer Group สำหรับ process แบบ parallel
At-least-once + idempotent = ค่า default ใน production
Exactly-once (Kafka transaction) ใช้ได้เฉพาะภายใน Kafka
Outbox + CDC สำหรับเขียน + publish แบบ atomic
Event Sourcing = เก็บ event ไม่เก็บ state, ใช้ snapshot ช่วย performance
CQRS + EDA + Event Sourcing = pattern microservices ยุคใหม่
Stream processing (Flink, Kafka Streams) สำหรับ analytics real-time
Schema registry สำหรับ schema evolution ที่ปลอดภัย
✅ Anti-patterns: coupling แน่น, chain ยาว, ลืม idempotency, over-engineer


← บทที่ 4 | บทที่ 6 → Modern Patterns