โหมดมืด
บทที่ 5 — Event-Driven Architecture
หลังจบบทนี้ คุณจะสามารถ:
- ออกแบบ 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 ZooKeeper external coordination service (เคยใช้คู่กับ Kafka) ❌ ถูกลบใน Kafka 4.0 KRaft built-in Raft consensus ใน Kafka เอง ✅ default, ใช้ใน 4.0+ ZAB protocol ของ 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 independently5. Kafka Producer — ฝั่งส่ง message
เดินทีละบรรทัด (สำหรับมือใหม่):
clientId— ชื่อเรียก app ของเรา (โผล่ใน log + metric ของ broker)brokers: ['kafka:9092']— รายการ "bootstrap broker"; client ใช้ตัวนี้เพื่อค้นหา broker ตัวอื่นใน clusterkafka.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 reconstructionAcks (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 producerIdempotent 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 + heartbeat7. 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 loss9. 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 changes10. 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 session11. 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 shiftSnapshot — ภาพรวมสำหรับ 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 table12. 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 formatSchema 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 documentation13. 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 user13.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 stateSnapshot 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 joinMaterialized 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 enrich17. ตัวอย่างจริง — 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 down19. 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 balanceConsumer 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 coordination22. 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 practiceDead 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 + countReprocessing จาก 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 trigger23. 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 semantics24. 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 alternativeBest 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 change25. 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 need26. 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 replay27. 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-bang28. ตัวอย่างจริง — 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 recovery30. 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 demands31. 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