⚙️ Software✍️ Khoa📅 20/04/2026☕ 8 phút đọc

System Design: Saga, Distributed Transactions và Outbox

8. Saga Pattern

8.1 Vấn đề Saga giải quyết

Microservices: mỗi service có DB riêng
→ Không thể dùng ACID transaction qua nhiều services
→ Cần cách maintain data consistency across services

Ví dụ: E-commerce order flow
  OrderService    → tạo order
  InventoryService → trừ stock
  PaymentService  → charge khách hàng
  ShippingService → tạo shipment

Nếu Payment thất bại sau khi Inventory đã trừ stock
→ Phải rollback Inventory!
→ Không có distributed transaction → dùng Saga

8.2 Saga = Sequence of Local Transactions + Compensations

Mỗi step:
  - Có một local transaction (trong 1 service)
  - Có một compensating transaction (để undo nếu cần)

Forward (happy path):
  T1: Create Order (OrderService)
  T2: Reserve Inventory (InventoryService)
  T3: Process Payment (PaymentService)
  T4: Create Shipment (ShippingService)
  → ALL succeed → Done ✅

Backward (compensation path, nếu T3 fail):
  C2: Release Inventory (InventoryService) ← compensate T2
  C1: Cancel Order (OrderService)          ← compensate T1
  T3: không cần compensate (đã fail)
  T4: không cần compensate (chưa chạy)

Compensating transaction:
  - Phải idempotent (có thể chạy nhiều lần safely)
  - Là business operation, không phải DB rollback
  - "Release inventory" không phải undo INSERT,
     mà là thêm stock lại
  - Có thể không đảo ngược hoàn toàn
    (ví dụ: email đã gửi → không "unsend" được,
     nhưng gửi apology email)

8.3 Choreography-based Saga

Không có central coordinator
Mỗi service lắng nghe events và react

  OrderService ──[OrderCreated]──────────────────────────────►
                                                               │
                 InventoryService ──────────────────────────► │
                 ◄── [OrderCreated]                           │
                 → Reserve stock                              │
                 ──[InventoryReserved]────────────────────────►
                                                               │
                                   PaymentService ────────── │
                                   ◄── [InventoryReserved]   │
                                   → Charge customer          │
                                   ──[PaymentProcessed]───────►
                                                               │
                                          ShippingService ── │
                                          ◄── [PaymentProcessed]
                                          → Create shipment   │
                                          ──[ShipmentCreated]─►

Failure flow:
  PaymentService: Payment FAIL
  ──[PaymentFailed]──────────────────────────────────────────►
  InventoryService: ◄── [PaymentFailed]
  → Release stock
  ──[InventoryReleased]────────────────────────────────────────►
  OrderService: ◄── [InventoryReleased]
  → Cancel order

Pros:
  Loose coupling
  Simple (no central coordinator)
  Services chỉ biết về events, không biết về nhau

Cons:
  Khó debug và trace (flow phân tán)
  Khó enforce ordering
  Risk of cyclic dependencies
  Không có global view của saga state

8.4 Orchestration-based Saga

Central Saga Orchestrator điều phối tất cả steps

Orchestrator biết toàn bộ flow và state của saga.

  ┌──────────────────────────────────────────────┐
  │            Saga Orchestrator                 │
  │  State: { sagaId, step, status, ... }        │
  └──────┬───────────────────────────────────────┘
         │ command          ▲ reply
         │                  │
  ┌──────▼──────┐    ┌──────┴──────┐    ┌───────────────┐
  │OrderService │    │  Inventory  │    │PaymentService │
  └─────────────┘    │  Service    │    └───────────────┘
                     └─────────────┘

Step 1: Orchestrator → OrderService: "CreateOrder"
        OrderService → Orchestrator: "OrderCreated" (success)

Step 2: Orchestrator → InventoryService: "ReserveInventory"
        InventoryService → Orchestrator: "InventoryReserved" (success)

Step 3: Orchestrator → PaymentService: "ProcessPayment"
        PaymentService → Orchestrator: "PaymentFailed" (failure!)

Step 4 (compensation):
        Orchestrator → InventoryService: "ReleaseInventory"
        InventoryService → Orchestrator: "InventoryReleased"
        Orchestrator → OrderService: "CancelOrder"
        OrderService → Orchestrator: "OrderCancelled"
        Orchestrator: Saga FAILED, compensated ✅

Saga State Machine:
  STARTED → INVENTORY_RESERVING → INVENTORY_RESERVED
  → PAYMENT_PROCESSING → PAYMENT_FAILED
  → INVENTORY_RELEASING → ORDER_CANCELLING → COMPENSATED

Pros:
  Clear flow, dễ debug
  Global state của saga
  Dễ thêm steps mới
  Dễ implement timeout và retry

Cons:
  Orchestrator có thể là bottleneck
  Risk: orchestrator = single point of failure
        (giải pháp: persist saga state, idempotent steps)
  Tight coupling đến orchestrator

8.5 Isolation trong Saga

Vấn đề: Saga KHÔNG có isolation như ACID transaction
  Các services khác có thể thấy intermediate state

Ví dụ:
  Saga step 2: inventory reserved (stock giảm)
  Concurrent query: thấy stock thấp hơn thực tế
  Nếu saga fail và compensate: stock về lại
  → Dirty read ở application level!

Countermeasures:

1. Semantic Lock:
   Đánh dấu record đang "pending" trong saga
   Clients khác thấy pending → chờ hoặc fail-fast
   ORDER_STATUS = 'SAGA_IN_PROGRESS'

2. Commutative Updates:
   Thiết kế updates có thể apply theo bất kỳ thứ tự
   Ví dụ: "add $10" và "subtract $5" commutative
   → Không cần lo về ordering

3. Pessimistic View:
   Assume saga có thể fail → hiển thị pessimistic data
   "Inventory: 5 available" (thực ra 8, 3 đang in saga)

4. Reread Values Before Use:
   Trước khi dùng value → reread để confirm vẫn valid
   Tránh dựa vào stale data từ trước

9. Distributed Transactions — 2PC & Alternatives

9.1 Two-Phase Commit (2PC) — Chi tiết

Participants: Coordinator + N participants (databases/services)

Phase 1 — Prepare (Voting):
  Coordinator → Participant A: "PREPARE txn-123"
  Coordinator → Participant B: "PREPARE txn-123"

  Participant A:
    - Thực thi transaction locally (nhưng CHƯA commit)
    - Ghi PREPARE record vào WAL (không thể rollback một chiều nữa)
    - Lock resources
    - → "YES" (prepared, sẵn sàng commit)
    hoặc → "NO" (không thể commit)

  Participant B: tương tự

Phase 2 — Commit hoặc Abort:
  Nếu ALL vote YES:
    Coordinator ghi COMMIT vào WAL
    Coordinator → A và B: "COMMIT"
    A, B: commit locally, release locks, acknowledge

  Nếu ANY vote NO hoặc timeout:
    Coordinator ghi ABORT vào WAL
    Coordinator → A và B: "ROLLBACK"
    A, B: rollback, release locks

Failure scenarios:
  Coordinator crash TRƯỚC Phase 2:
    A và B đang ở "prepared" state (uncertain)
    Resources bị LOCK → blocking indefinitely!
    → Phải đợi coordinator recover và check WAL

  Participant crash TRONG Phase 2:
    Khi recover: đọc WAL → thấy PREPARE → hỏi coordinator → commit hoặc rollback
    → "Termination protocol"

  Network partition:
    Coordinator không nhận được votes
    → Timeout → Abort

Vấn đề cốt lõi của 2PC: BLOCKING PROTOCOL
  Nếu coordinator fail sau PREPARE → participants bị block
  Không có way để participants tự quyết định (không biết vote của nhau)

9.2 Three-Phase Commit (3PC) — Non-blocking

Thêm "Pre-Commit" phase để giải quyết blocking:

Phase 1 — CanCommit: giống Phase 1 của 2PC (vote)
Phase 2 — PreCommit: coordinator thông báo quyết định TRƯỚC khi commit
Phase 3 — DoCommit: actual commit

Nếu participant không nhận PreCommit → tự rollback (timeout-based)
Nếu participant nhận PreCommit nhưng không DoCommit
  → Tự commit (vì biết coordinator đã quyết định commit)

Vẫn không hoàn hảo:
  Cần timeout mechanism
  Network partition → split-brain vẫn có thể xảy ra
  Phức tạp hơn 2PC đáng kể
  → Ít được dùng trong thực tế

9.3 Consensus Protocols — Paxos & Raft

Consensus = tất cả nodes đồng ý về một giá trị,
            kể cả khi một số nodes fail

Paxos (Leslie Lamport, 1989):
  Roles: Proposer, Acceptor, Learner
  Phases: Prepare → Promise → Accept → Accepted
  Nổi tiếng là khó hiểu và implement đúng

Raft (2013) — "Paxos cho người bình thường":
  Roles: Leader, Follower, Candidate
  Đảm bảo: chỉ 1 Leader tại một thời điểm

  Leader Election:
    Mọi node bắt đầu là Follower
    Nếu không nhận heartbeat từ Leader trong timeout
    → Chuyển thành Candidate → gửi RequestVote
    → Nếu nhận majority votes → trở thành Leader
    → Terms: mỗi election là 1 term

  Log Replication:
    Client gửi command đến Leader
    Leader append vào log → gửi AppendEntries đến Followers
    Khi majority confirm → Leader commit → apply to state machine
    → Inform followers to commit

  Safety:
    Node không thể vote cho candidate có log outdated hơn mình
    → Đảm bảo committed entries không bị mất

  Used in: etcd (Kubernetes), CockroachDB, TiKV, Consul

9.4 Google Spanner — TrueTime

Spanner = globally distributed DB với external consistency
(stronger than linearizability across datacenters!)

Vấn đề: Clock skew giữa các datacenters
  Node A (US): timestamp = 1000ms
  Node B (EU): timestamp = 999ms (clock slightly behind)
  → Làm sao biết thứ tự events?

TrueTime API:
  Mỗi server có GPS + atomic clock
  TrueTime.now() trả về [earliest, latest] interval
  Đảm bảo: real time nằm trong interval

Commit Wait:
  Trước khi commit transaction:
  → Đợi cho đến khi TrueTime.now().earliest > commit_timestamp
  → Đảm bảo tất cả concurrent transactions có timestamp cũ hơn

Result:
  Commits globally ordered by real time
  No distributed locking needed!
  External consistency: nếu T1 commit trước T2 start
  → T2 luôn thấy T1's changes

Cost: commit latency = 2× clock uncertainty (~7ms)
      → Acceptable cho global DB

10. Outbox Pattern

10.1 Vấn đề Dual Write

Cần: Update DB và publish event ATOMICALLY
     (không thể để DB updated nhưng event không publish, và vice versa)

Naive approach (WRONG):
  tx.begin()
  db.update(order)
  tx.commit()        ← nếu crash ở đây...
  eventBus.publish() ← event bị mất!

  hoặc:
  eventBus.publish() ← event published...
  tx.begin()
  db.update(order)
  tx.commit()        ← nếu fail → DB không update nhưng event đã gửi!

10.2 Outbox Pattern Solution

Trong cùng 1 DB transaction:
  - Update business tables
  - INSERT event vào "outbox" table

Separate process (Message Relay) reads outbox và publish đến message broker.

┌─────────────────────────────────────────────────────────────┐
│  Database Transaction (ATOMIC)                              │
│  UPDATE orders SET status='shipped' WHERE id=123            │
│  INSERT INTO outbox (event_type, payload, status)           │
│    VALUES ('OrderShipped', '{...}', 'PENDING')              │
└──────────────────────────┬──────────────────────────────────┘
                           │ committed atomically
                           │
  ┌────────────────────────▼──────────────────────────────┐
  │  Outbox Table                                         │
  │  id | event_type   | payload | status  | created_at  │
  │  1  | OrderShipped | {...}   | PENDING | 2024-01-01  │
  └────────────────────────┬──────────────────────────────┘
                           │ poll / CDC
  ┌────────────────────────▼──────────────────────────────┐
  │  Message Relay (background process)                   │
  │  - Đọc PENDING events từ outbox                       │
  │  - Publish đến Kafka/RabbitMQ                         │
  │  - Mark as PROCESSED (or delete)                      │
  └────────────────────────┬──────────────────────────────┘
                           │
  ┌────────────────────────▼──────────────────────────────┐
  │  Message Broker (Kafka, RabbitMQ)                     │
  └───────────────────────────────────────────────────────┘

Guarantee: AT-LEAST-ONCE delivery
  (event có thể published nhiều lần nếu relay crash sau publish
   nhưng trước khi mark PROCESSED)
→ Consumers phải idempotent!

10.3 Change Data Capture (CDC)

Thay vì poll outbox table → dùng CDC để read DB transaction log

PostgreSQL: logical replication slot
MySQL: binlog
→ Debezium (phổ biến nhất) đọc transaction log → publish đến Kafka

CDC Pipeline:
  PostgreSQL WAL/binlog
       │ Debezium connector
       ▼
  Kafka (event stream)
       │ Consumer
       ▼
  Downstream services

Pros:
  Real-time (không phải polling)
  Không cần modify application code (read log directly)
  Capture ALL changes (not just what app explicitly publishes)
  Low overhead trên DB

Cons:
  Phụ thuộc vào internal DB format
  Schema changes phức tạp
  Cần manage Debezium cluster