บทที่ 1 · Part 1 — Foundation

Why Temporal

ปัญหา distributed transaction ในระบบ wallet, Durable Execution คืออะไร และ Temporal ไม่ได้แก้อะไรบ้าง

ก่อนจะเรียนว่า Temporal ทำงานยังไง ต้องเข้าใจก่อนว่ามันแก้ปัญหาอะไร — และที่สำคัญกว่านั้นคือมันไม่ได้แก้ปัญหาอะไร

จบบทนี้คุณจะ

  • อธิบายได้ว่าปัญหา "ไม่รู้ว่าเงินตัดไปหรือยัง" เกิดจากอะไร
  • เข้าใจคำว่า Durable Execution และต่างจาก message queue อย่างไร
  • ตัดสินใจได้ว่างานไหนควรใช้ Temporal งานไหนไม่ควร

ปัญหาจริง: เติมเงินเข้า wallet

สมมติ flow การเติมเงิน (top-up) เข้า wallet ผ่านการหักบัญชีธนาคาร ซึ่งเป็นเคสพื้นฐานที่สุดของ e-wallet ในไทย:

ใน happy path ทุกอย่างเรียบร้อย ปัญหาคือ production ไม่ค่อยมี happy path

เกิดอะไรขึ้นผลถ้าไม่มีอะไรมาช่วย
ขั้นที่ 3 ส่ง request ไปธนาคารแล้ว แต่ timeout ก่อนได้ responseไม่รู้ว่าธนาคารหักเงินไปแล้วหรือยัง จะ retry ก็เสี่ยงหักซ้ำ จะไม่ retry ก็เสี่ยงลูกค้าเสียเงินฟรี
ขั้นที่ 3 สำเร็จ แต่ pod ถูก OOM-kill ก่อนถึงขั้นที่ 4เงินออกจากบัญชีธนาคารแล้ว แต่ไม่เข้า wallet — ต้องมีคนมาตามเก็บด้วยมือ
ธนาคาร ล่ม 3 ชั่วโมง ช่วงกลางคืนต้องมีตาราง retry + cron + backoff เขียนเอง และต้องกันไม่ให้ retry ชน rate limit ธนาคาร
ธนาคารตอบกลับแบบ asynchronous ผ่าน callback ในอีก 30 นาทีต้องเก็บ state ค้างไว้ใน DB แล้วมีอีก service มาปลุกต่อ — logic กระจายเป็นเสี่ยง
ต้องรอ OTP จากผู้ใช้ 5 นาที ระหว่างขั้นที่ 2 กับ 3process รอไม่ได้ ต้องตัดเป็น state machine แล้วเก็บลง DB

ทางแก้แบบดั้งเดิม และต้นทุนของมัน

ทีมส่วนใหญ่แก้ปัญหานี้ด้วยชุดเครื่องมือที่คุ้นเคย: ตาราง transactions ที่มีคอลัมน์ status, transactional outbox, message queue (Kafka/RabbitMQ), cron job ไล่ retry, dead-letter queue, และตาราง idempotency_keys

วิธีนี้ ใช้ได้จริง แต่มีต้นทุนที่มักถูกประเมินต่ำ:

  • State machine กระจายไปทั่ว — flow เดียวถูกหั่นเป็น handler หลายตัวคนละไฟล์ คนอ่านโค้ดใหม่ตามไม่ทันว่าลำดับจริงคืออะไร
  • โค้ด infrastructure กลบโค้ด business — logic การเติมเงินจริงอาจมี 30 บรรทัด แต่โค้ดรอบๆ ที่จัดการ retry/timeout/resume มี 300 บรรทัด
  • Debug ยาก — เวลามีเคสหลุด ต้องไล่ log ข้าม 4 service เพื่อสร้างภาพว่าเกิดอะไรขึ้นตามลำดับ
  • Timer ไม่ทน — "รอ 5 นาทีแล้วยกเลิก" ที่เขียนด้วย in-memory timer จะหายไปเมื่อ pod restart

Durable Execution คืออะไร

Temporal เสนอวิธีที่ต่างออกไป: เขียน flow ทั้งหมดเป็นฟังก์ชันเดียวแบบ sequential ธรรมดา แล้วให้ platform รับผิดชอบเรื่องความทนทานให้

func TopUpWorkflow(ctx, req) error {
    CheckLimit(ctx, req)              // ถ้า service ล่ม → retry อัตโนมัติ
    ref := ChargeBank(ctx, req)       // ถ้า pod ตายตรงนี้ → กลับมาทำต่อจากจุดนี้
    CreditWallet(ctx, req, ref)       // ไม่ต้องกลัวว่า ChargeBank จะถูกเรียกซ้ำ
    Notify(ctx, req)
    return nil
}

หัวใจคือคำว่า Durable Execution: สถานะการทำงานของฟังก์ชัน — ค่าตัวแปร, ตำแหน่งบรรทัดที่รันถึง, timer ที่ตั้งค้างไว้, และผลลัพธ์ของทุก step ที่ทำไปแล้ว — ถูก persist ลง storage อัตโนมัติ

ถ้า process ตายกลางคัน Temporal จะให้ worker ตัวใหม่ สร้างสถานะเดิมขึ้นมาใหม่ แล้วทำงานต่อจากจุดที่ค้าง โดยไม่เรียก step ที่สำเร็จไปแล้วซ้ำ กลไกนี้ชื่อ history replay และเป็นเนื้อหาหลักของบทที่ 4

ผลลัพธ์ที่จับต้องได้

โค้ดที่คุณเขียนเหลือแค่ business logic ส่วน retry / timeout / resume-after-crash / durable timer กลายเป็น configuration แทนที่จะเป็นโค้ดที่ต้อง maintain เอง

Temporal ต่างจาก message queue อย่างไร

Message Queue (Kafka, SQS)Temporal
หน่วยของงานmessage 1 ใบทั้ง flow ตั้งแต่ต้นจนจบ
ลำดับขั้นตอนอยู่ในหัวคนเขียน กระจายตาม consumerอยู่ในโค้ดฟังก์ชันเดียว อ่านแล้วเห็นทั้งหมด
State ระหว่างขั้นคุณต้องเก็บเองใน DBplatform เก็บให้อัตโนมัติ
Retryเขียนเอง / ตั้งใน consumerเป็น policy ต่อ step (exponential backoff ในตัว)
รอเป็นวัน/เดือนทำได้ยาก ต้องมี scheduler แยกเขียน workflow.Sleep(ctx, 30*24*time.Hour)
มองเห็นสถานะปัจจุบันต้องสร้าง dashboard เองเห็น timeline ทุก event ใน Web UI ทันที

ทั้ง 2 อย่างไม่ได้แทนกัน — หลายระบบใช้ทั้งคู่ เช่น รับ event จาก Kafka แล้ว start Temporal workflow เพื่อจัดการ flow ที่มีหลายขั้นตอน

สิ่งที่ Temporal ไม่ได้แก้ให้

สำคัญมากสำหรับระบบการเงิน

ความเข้าใจผิดเหล่านี้ทำให้ทีมออกแบบระบบผิดตั้งแต่ต้น

Temporal ไม่ใช่ ledger

Event History ของ Temporal คือ log ว่า โปรแกรมทำอะไรไปบ้าง ไม่ใช่ บัญชีว่าใครมีเงินเท่าไร คุณยังต้องมี double-entry ledger ในฐานข้อมูลของคุณเอง พร้อม constraint และ audit trail ตามที่ regulator ต้องการ Temporal ทำหน้าที่ orchestrate การเขียน ledger ให้เกิดขึ้นครบถ้วน แต่ไม่ใช่ที่เก็บยอดเงิน

Temporal ไม่ได้ทำให้ bank API เป็น idempotent ให้

Temporal รับประกันว่าจะ retry ให้ แต่ถ้า bank API ของคุณหักเงินซ้ำเมื่อได้รับ request เดิม 2 ครั้ง การ retry ก็คือการหักเงินซ้ำ คุณต้องส่ง idempotency key ไปกับทุก request เสมอ (บทที่ 5 ลงรายละเอียดเรื่องนี้)

Temporal ไม่ให้ atomicity ข้ามระบบ

ไม่มี distributed transaction แบบ two-phase commit ให้ใช้ ถ้าหักเงินธนาคารสำเร็จแล้วเครดิต wallet ไม่ได้ คุณต้อง ชดเชย (compensate) ด้วยการคืนเงิน ซึ่งคือ Saga pattern ในบทที่ 6

Temporal เพิ่มของที่ต้องดูแล

ถ้า self-host คุณต้องดูแล cluster + ฐานข้อมูล (Cassandra/PostgreSQL/MySQL) + Elasticsearch สำหรับ visibility ถ้าใช้ Temporal Cloud ก็มีค่าใช้จ่ายและต้องผ่านกระบวนการ vendor assessment ซึ่งสำหรับสถาบันการเงินไม่ใช่เรื่องเล็ก

เมื่อไหร่ควรใช้ / ไม่ควรใช้

เหมาะมากไม่เหมาะ
Flow หลายขั้นข้ามหลายระบบ ที่ล้มกลางคันแล้วต้องกู้ให้ถูก (top-up, โอนออก, settlement)CRUD API ธรรมดา อ่าน/เขียน DB ตัวเดียวจบ
งานที่ต้องรอนาน — รอ OTP, รอ callback ธนาคาร, รอ approval, รอ T+1Request ที่ต้องตอบใน 50 ms (Temporal มี overhead ระดับหลาย ms ต่อ step)
งานที่ต้องตอบ auditor ได้ว่า "transaction นี้เกิดอะไรขึ้นบ้างเรียงตามเวลา"Stream processing ปริมาณสูงมากแบบ per-event (ใช้ Kafka/Flink เหมาะกว่า)
Batch ที่ต้องทนล้ม เช่น จ่ายเงินเดือน 50,000 รายการ แล้วล้มที่รายการ 31,000งาน compute หนักล้วนๆ ที่ไม่มี orchestration (ใช้ job runner ธรรมดา)
Long-running entity เช่น สัญญาผ่อนชำระ 12 งวด, subscription รายเดือนทีมที่ยังไม่มีกำลังดูแล component ใหม่ และ flow ปัจจุบันก็ไม่ได้มีปัญหา

สรุปประโยคเดียว

Temporal เหมาะเมื่อ ต้นทุนของการที่ flow ล้มกลางคันแล้วกู้ผิด สูงกว่าต้นทุนของการดูแล component เพิ่มอีก 1 ตัว — ซึ่งในระบบที่เคลื่อนย้ายเงินจริง มักจะสูงกว่าเสมอ