ЁЯПл The SchoolтА║ЁЯУР System DesignтА║ЁЯдЭ рдзрдбрд╛ 11 тАФ Services рдУрд▓рд╛рдВрдбреВрди transactions: рд╢рд╛рд│реЗрдЪреНрдпрд╛ рд╕рд╣рд▓реАрдЪреЗ booking
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯдЭ рдзрдбрд╛ 11 тАФ Services рдУрд▓рд╛рдВрдбреВрди transactions: рд╢рд╛рд│реЗрдЪреНрдпрд╛ рд╕рд╣рд▓реАрдЪреЗ booking

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 18 рдкреИрдХреА рдзрдбрд╛ 11 ┬╖ рдкреБрдвреАрд▓: lesson-12-rate-limiting


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ10, рдЖрдгрд┐ рдХрд╛рдо рдЕрдиреЗрдХ services рдордзреНрдпреЗ рдкрд╕рд░рд▓реЗрд▓реЗ рдЕрд╕рддрд╛рдирд╛ "рд╕рдЧрд│реЗ рдХрд┐рдВрд╡рд╛ рдХрд╛рд╣реАрдЪ рдирд╛рд╣реА", рдЬрд┐рдереЗ рдкреНрд░рддреНрдпреЗрдХреАрдЪрд╛ рд╕реНрд╡рддрдГрдЪрд╛ database рдЕрд╕рддреЛ. рддреБрдореНрд╣реА рд╢рд┐рдХрддрд╛ рдХреА local transaction рдкреБрд░реЗрд╕рд╛ рдХрд╛ рдирд╛рд╣реА, two-phase commit (2PC) рдЖрдгрд┐ рддреЛ рдХрд╛ рдЕрдбрдХрддреЛ, sagas (orchestration рдЖрдгрд┐ choreography), compensations, idempotency, рдЖрдгрд┐ рдкреБрдиреНрд╣рд╛ рдзрдбрд╛ 08 рдордзреАрд▓ outbox.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдХрддрд░рд┐рдирд╛ рддрд┐рдЪреНрдпрд╛ рдореБрд▓реАрд╕рд╛рдареА рд╢рд╛рд│реЗрдЪреА рд╕рд╣рд▓ book рдХрд░рддреЗ. рддреАрди рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рдВрдЪреЗ рдПрдХрдордд рд╣рд╡реЗ: seats рдХрд╛рд░реНрдпрд╛рд▓рдп, fees рдХрд╛рд░реНрдпрд╛рд▓рдп рдЖрдгрд┐ bus рдХрд╛рд░реНрдпрд╛рд▓рдп. рдкреНрд░рддреНрдпреЗрдХ рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рдЪреА рд╕реНрд╡рддрдГрдЪреА рдиреЛрдВрджрд╡рд╣реА рдЖрд╣реЗ.

рдпреЛрдЬрдирд╛ A тАФ рдлреЛрди рдХреЙрд▓ (2PC). рджреАрдкрд┐рдХрд╛ рддрд┐рдШрд╛рдВрдирд╛рд╣реА рдлреЛрди рдХрд░рддреЗ: "рддреБрдореНрд╣реА рд╣реЗ рдХрд░реВ рд╢рдХрддрд╛ рдХрд╛? рд╣реЛ рдЕрд╕реЗрд▓ рддрд░ рддреЗ рд░рд╛рдЦреВрди рдареЗрд╡рд╛ рдЖрдгрд┐ рдорд╛рдЭреНрдпрд╛ рд╢рдмреНрджрд╛рдЪреА рд╡рд╛рдЯ рдкрд╛рд╣рд╛." рддрд┐рдШреЗрд╣реА рд╣реЛ рдореНрд╣рдгрддрд╛рдд рдЖрдгрд┐ рд░рд╛рдЦреВрди рдареЗрд╡рддрд╛рдд: seat рд░рд╛рдЦрд▓реА, рдкреИрд╕реЗ рд░рд╛рдЦрд▓реЗ, bus рдордзрд▓реА рдЬрд╛рдЧрд╛ рд░рд╛рдЦрд▓реА. рдордЧ рджреАрдкрд┐рдХрд╛ рдореНрд╣рдгрддреЗ "рдЪрд▓рд╛!" рдЖрдгрд┐ рддрд┐рдШреЗрд╣реА рддреЗ рд▓рд┐рд╣реВрди рдШреЗрддрд╛рдд. рдкрдг "рд░рд╛рдЦреВрди рдареЗрд╡рд╛" рдирдВрддрд░ рджреАрдкрд┐рдХрд╛рдЪрд╛ рдлреЛрди рдмрдВрдж рдкрдбрд▓рд╛, рддрд░ рддрд┐рдиреНрд╣реА рдХрд╛рд░реНрдпрд╛рд▓рдпреЗ рд╡рд╛рдЯ рдкрд╛рд╣рдд рд░рд╛рд╣рддрд╛рдд. Seat рд░рд╛рдЦрд▓реЗрд▓реАрдЪ рд░рд╛рд╣рддреЗ. рдкреИрд╕реЗ рд░рд╛рдЦрд▓реЗрд▓реЗрдЪ рд░рд╛рд╣рддрд╛рдд. рджреБрд╕рд░реЗ рдХреЛрдгреАрдЪ рддреЗ рд╡рд╛рдкрд░реВ рд╢рдХрдд рдирд╛рд╣реА. рджреАрдкрд┐рдХрд╛ рдкрд░рдд рдлреЛрди рдХрд░реЗрдкрд░реНрдпрдВрдд рддреЗ рдерд╛рдВрдмрддрд╛рдд.

рдпреЛрдЬрдирд╛ B тАФ undo рдпрд╛рджреА (saga). рдкреНрд░рддреНрдпреЗрдХ рдХрд╛рд░реНрдпрд╛рд▓рдп рдЖрдкрд▓рд╛ рднрд╛рдЧ рд▓рдЧреЗрдЪ рдХрд░рддреЗ рдЖрдгрд┐ рд╕реЛрдмрдд рддреЛ undo рдХрд╕рд╛ рдХрд░рд╛рдпрдЪрд╛ рд╣реЗ рд▓рд┐рд╣реВрди рдареЗрд╡рддреЗ. Seats: рдЭрд╛рд▓реЗ (undo: seat рдореЛрдХрд│реА рдХрд░рд╛). Fees: рдЭрд╛рд▓реЗ (undo: рдкреИрд╕реЗ рдкрд░рдд). Bus: "рдорд╛рдл рдХрд░рд╛, bus рднрд░рд▓реА рдЖрд╣реЗ." рдореНрд╣рдгреВрди рдХрд╛рд░реНрдпрд╛рд▓рдп рдЙрд▓рдЯ рдХреНрд░рдорд╛рдиреЗ рдорд╛рдЧреЗ рдЬрд╛рддреЗ: рдкреИрд╕реЗ рдкрд░рдд рдХрд░рд╛, seat рдореЛрдХрд│реА рдХрд░рд╛. рдХреЛрдгреАрдЪ рдерд╛рдВрдмрд▓реЗ рдирд╛рд╣реА. рдереЛрдбреНрдпрд╛ рд╡реЗрд│рд╛рд╕рд╛рдареА seat рдШреЗрддрд▓реЗрд▓реА рд╣реЛрддреА рдЖрдгрд┐ рдкреИрд╕реЗ рдХрд╛рдкрд▓реЗрд▓реЗ рд╣реЛрддреЗ тАФ рдЖрдгрд┐ рдордЧ рджреЛрдиреНрд╣реА undo рдЭрд╛рд▓реЗ.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    subgraph tpc["ЁЯУЮ 2PC: one coordinator"]
      co["coordinator"] -->|"1 prepare"| s1["seats"]
      co -->|"1 prepare"| p1["fees"]
      co -->|"1 prepare"| b1["bus"]
      co -.->|"2 commit or abort"| s1
      dead["ЁЯТА coordinator dies after prepare<br/>тЖТ BLOCKED, locks held"]
    end
    subgraph sg["ЁЯУЛ saga: local steps + undo"]
      a["тЬУ reserve seat"] --> b["тЬУ charge тВ╣500"] --> c["тЬЧ book the bus"]
      c -->|"compensate"| rb["тЖй refund тВ╣500"] --> rs["тЖй release seat"]
    end

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/system-design/lesson-diagrams.html#l11

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг microservices data рд╡рд┐рднрд╛рдЧрддрд╛рдд. рдирд┐рдпреЛрдЬрди рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рд▓рд╛ рддрд░реАрд╣реА рд╡рдЪрди рджреНрдпрд╛рдпрдЪреЗ рдЕрд╕рддреЗ рдХреА "рд╕рд╣рд▓ рдПрдХрддрд░ рдкреВрд░реНрдг book рд╣реЛрддреЗ рдХрд┐рдВрд╡рд╛ рдЕрдЬрд┐рдмрд╛рдд рд╣реЛрдд рдирд╛рд╣реА". 2PC рд╣реЗ рд╡рдЪрди рджреЗрддреЛ, рдкрдг рддреЛ services рдУрд▓рд╛рдВрдбреВрди locks рдзрд░реВрди рдареЗрд╡рддреЛ рдЖрдгрд┐ coordinator рдерд╛рдВрдмрд▓рд╛ рдХреА рдерд╛рдВрдмрддреЛ. Saga рдХрдзреАрдЪ global lock рдзрд░рдд рдирд╛рд╣реА рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ service рд╕реНрд╡рддрдВрддреНрд░ рдареЗрд╡рддреЗ тАФ рддреНрдпрд╛рдЪреА рдХрд┐рдВрдордд рдореНрд╣рдгрдЬреЗ рдереЛрдбреНрдпрд╛ рд╡реЗрд│рд╛рдЪреА рдПрдХ рдордзрд▓реА рдЕрд╡рд╕реНрдерд╛ рдЬреА рдЗрддрд░ рд╡рд╛рдЪрдХрд╛рдВрдирд╛ рджрд┐рд╕реВ рд╢рдХрддреЗ (seat рдШреЗрддрд▓реА, рдордЧ рдореЛрдХрд│реА рдХреЗрд▓реА). рдореНрд╣рдгреВрди рдХрд╛рд░реНрдпрд╛рд▓рдп рдХрд╛рд╣реАрд╣реА рдмрд╛рдВрдзрдгреНрдпрд╛рдЖрдзреА рджреЛрдиреНрд╣реА рд▓рд┐рд╣рд┐рддреЗ: рдкрд╛рдпрд▒реНрдпрд╛ рдЖрдгрд┐ рддреНрдпрд╛рдВрдЪреЗ undo.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

design/blocks.py рдордзреНрдпреЗ:

design/demo.py рдордзреАрд▓ transactions() рд╢рд╛рд│реЗрдЪреА рд╕рд╣рд▓ book рдХрд░рддреЛ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 design/demo.py transactions
python3 - <<'EOF'
import sys; sys.path.insert(0, "design"); from blocks import two_phase_commit, saga
for votes in ([True] * 3, [True, False, True], [False] * 3):
    print(votes, "тЖТ", two_phase_commit(votes), "| coordinator dead тЖТ", two_phase_commit(votes, coordinator_alive=False)[:7])
steps = [("reserve seat", "release seat"), ("charge Katrina тВ╣500", "refund Katrina тВ╣500"), ("book the bus", "cancel the bus")]
for f in (None, 0, 1, 2):
    s, log = saga(steps, fail_at=f)
    print(f"fail_at={f!s:<4} тЖТ {s:<11} {' ┬╖ '.join(log)}")
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

transactions рд╣реЗ рдЫрд╛рдкрддреЛ:

тФАтФА a school-trip booking touches 3 services: seats, payment, bus
   2PC, all vote yes тЖТ COMMIT ┬╖ one votes no тЖТ ABORT
   2PC, the coordinator dies after 'prepare' тЖТ BLOCKED тАФ participants hold locks until the coordinator returns
   saga, the bus is full тЖТ COMPENSATED: тЬУ reserve seat ┬╖ тЬУ charge Katrina тВ╣500 ┬╖ тЬЧ book the bus ┬╖ тЖй refund Katrina тВ╣500 ┬╖ тЖй release seat
   sagas trade 'all at once' for 'eventually consistent, with an undo for every step'

рддреБрдордЪрд╛ snippet рд╣реЗ рдЫрд╛рдкрддреЛ:

[True, True, True] тЖТ COMMIT | coordinator dead тЖТ BLOCKED
[True, False, True] тЖТ ABORT | coordinator dead тЖТ BLOCKED
[False, False, False] тЖТ ABORT | coordinator dead тЖТ BLOCKED
fail_at=None тЖТ DONE        тЬУ reserve seat ┬╖ тЬУ charge Katrina тВ╣500 ┬╖ тЬУ book the bus
fail_at=0    тЖТ COMPENSATED тЬЧ reserve seat
fail_at=1    тЖТ COMPENSATED тЬУ reserve seat ┬╖ тЬЧ charge Katrina тВ╣500 ┬╖ тЖй release seat
fail_at=2    тЖТ COMPENSATED тЬУ reserve seat ┬╖ тЬУ charge Katrina тВ╣500 ┬╖ тЬЧ book the bus ┬╖ тЖй refund Katrina тВ╣500 ┬╖ тЖй release seat

Tests рдордзреНрдпреЗ тЬЕ L11 2PC blocks without its coordinator; a saga compensates in reverse рдЕрд╕рддреЗ.

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

Coordinator рдЬрд┐рд╡рдВрдд рдЕрд╕реЗрдкрд░реНрдпрдВрдд 2PC рд╕реЛрдкрд╛ рдЖрд╣реЗ: рд╕рдЧрд│реЗ рд╣реЛ тЖТ COMMIT, рдПрдХ рдирд╛рд╣реА тЖТ ABORT. Prepare рдирдВрддрд░ рддреЛ рдЧреЗрд▓рд╛, рддрд░ рдЙрддреНрддрд░ BLOCKED тАФ locks рдзрд░рд▓реЗрд▓реЗ, рдХрд╛рдо рдерд╛рдВрдмрд▓реЗрд▓реЗ. Saga рдХрдзреАрдЪ рдЕрдбрдХрдд рдирд╛рд╣реА: рдкрд╛рдпрд░реА 0 рд╡рд░ рдЕрдкрдпрд╢ рдЖрд▓реЗ рддрд░ рдХреЛрдгрддрд╛рд╣реА undo рд▓рд╛рдЧрдд рдирд╛рд╣реА, рдкрд╛рдпрд░реА 1 рд╡рд░ рдЕрдкрдпрд╢ рдПрдХ рдкрд╛рдпрд░реА undo рдХрд░рддреЗ, рдкрд╛рдпрд░реА 2 рд╡рд░ рдЕрдкрдпрд╢ рджреЛрди undo рдХрд░рддреЗ, рдиреЗрд╣рдореА рд╕рд░реНрд╡рд╛рдд рдирд╡реА рдЖрдзреА. Saga рдХрд┐рддреА рдкреБрдвреЗ рдЧреЗрд▓рд╛ рддреНрдпрд╛рдиреБрд╕рд╛рд░ compensations рдЪреА рд╕рдВрдЦреНрдпрд╛ рд╡рд╛рдврддреЗ тАФ рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ compensation рджреЛрдирджрд╛ рдЪрд╛рд▓рд╡рдгреЗ рд╕реБрд░рдХреНрд╖рд┐рдд рдЕрд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ AWS Step Functions рдордзреНрдпреЗ рдПрдХ orchestrated saga. рдкреНрд░рддреНрдпреЗрдХ task рдордзреНрдпреЗ рдПрдХ Catch рдЕрд╕рддреЛ рдЬреЛ compensations рдХрдбреЗ, рдЙрд▓рдЯ рдХреНрд░рдорд╛рдиреЗ, рдЙрдбреА рдорд╛рд░рддреЛ (Amazon States Language, рдЫреЛрдЯреЗ рдХреЗрд▓реЗрд▓реЗ):

{
  "StartAt": "ReserveSeat",
  "States": {
    "ReserveSeat": { "Type": "Task", "Resource": "arn:aws:lambda:ap-south-1:111122223333:function:reserve-seat",
      "Catch": [{ "ErrorEquals": ["States.ALL"], "Next": "Failed" }], "Next": "ChargeFee" },
    "ChargeFee":   { "Type": "Task", "Resource": "arn:aws:lambda:ap-south-1:111122223333:function:charge-fee",
      "Catch": [{ "ErrorEquals": ["States.ALL"], "Next": "ReleaseSeat" }], "Next": "BookBus" },
    "BookBus":     { "Type": "Task", "Resource": "arn:aws:lambda:ap-south-1:111122223333:function:book-bus",
      "Catch": [{ "ErrorEquals": ["States.ALL"], "Next": "RefundFee" }], "End": true },
    "RefundFee":   { "Type": "Task", "Resource": "arn:aws:lambda:ap-south-1:111122223333:function:refund-fee",
      "Retry": [{ "ErrorEquals": ["States.ALL"], "MaxAttempts": 5, "BackoffRate": 2 }], "Next": "ReleaseSeat" },
    "ReleaseSeat": { "Type": "Task", "Resource": "arn:aws:lambda:ap-south-1:111122223333:function:release-seat",
      "Retry": [{ "ErrorEquals": ["States.ALL"], "MaxAttempts": 5, "BackoffRate": 2 }], "Next": "Failed" },
    "Failed":      { "Type": "Fail", "Error": "TripNotBooked" }
  }
}

Compensations retry рдХрд░рддрд╛рдд (рддреА рдкреВрд░реНрдг рд╡реНрд╣рд╛рдпрд▓рд╛рдЪ рд╣рд╡реАрдд), рдореНрд╣рдгреВрди рддреА idempotent рд╣рд╡реАрдд. рдкреНрд░рддреНрдпреЗрдХ service рдордзреНрдпреЗ, рдЖрдзреАрдЪ рд╣рд╛рддрд╛рд│рд▓реЗрд▓реНрдпрд╛ keys рд▓рдХреНрд╖рд╛рдд рдареЗрд╡рд╛:

CREATE TABLE handled_requests (idempotency_key text PRIMARY KEY, result jsonb, created_at timestamptz DEFAULT now());
-- inside the same transaction as the refund:
INSERT INTO handled_requests (idempotency_key, result) VALUES ('trip-42-refund', '{"refunded": 500}')
ON CONFLICT (idempotency_key) DO NOTHING;   -- 0 rows inserted тЖТ it was done before; return the saved result

2PC рдПрдХрд╛рдЪ database рдХреБрдЯреБрдВрдмрд╛рдЪреНрдпрд╛ рдЖрдд рдЦрд░реЛрдЦрд░ рдЕрд╕рддреЛ. PostgreSQL рдордзреНрдпреЗ prepared transactions рдЖрд╣реЗрдд (default рдиреЗ рдмрдВрдж тАФ max_prepared_transactions = 0), рдЬреЗ transaction managers рд╡рд╛рдкрд░рддрд╛рдд:

BEGIN; UPDATE seats SET taken = true WHERE trip = 42 AND seat = 17;
PREPARE TRANSACTION 'trip-42';     -- phase 1: saved to disk, locks held
COMMIT PREPARED 'trip-42';         -- phase 2 (or ROLLBACK PREPARED 'trip-42')

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рд╡рд┐рд╕рд░рд▓реЗрд▓рд╛ prepared transaction locks рдзрд░реВрди рдареЗрд╡рддреЛ рдЖрдгрд┐ VACUUM рдерд╛рдВрдмрд╡рддреЛ. SELECT * FROM pg_prepared_xacts; рддрдкрд╛рд╕рд╛. Sagas рд╕рд╛рдареА, compensation рдорд╛рд░реНрдЧрд╛рдиреЗ рд╕рдВрдкрдгрд╛рд▒реНрдпрд╛ executions рд╡рд░ alarm рд▓рд╛рд╡рд╛ тАФ рдкреНрд░рддреНрдпреЗрдХ рдореНрд╣рдгрдЬреЗ charge рдЖрдгрд┐ refund рджреЛрдиреНрд╣реА рдкрд╛рд╣рд┐рд▓реЗрд▓рд╛ рдПрдХ рдкрд╛рд▓рдХ.

тПня╕П рдкреБрдвреЗ

рдХрд╛рд░реНрдпрд╛рд▓рдп рдЖрддрд╛ рдПрдХрдордд рдХрд░реВ рд╢рдХрддреЗ рдЖрдгрд┐ undo рдХрд░реВ рд╢рдХрддреЗ. рдкрдг рдПрдХрд╛ рдкрд╛рд▓рдХрд╛рдЪреНрдпрд╛ app рдиреЗ рд╕реЗрдХрдВрджрд╛рд▓рд╛ 1,000 requests рдкрд╛рдард╡рд▓реНрдпрд╛ рддрд░? Rate limiting.

git checkout lesson-12-rate-limiting

ЁЯдЭ Lesson 11 тАФ Transactions across services: the school trip booking

ЁЯУН You are here: Lesson 11 of 18 ┬╖ Next: lesson-12-rate-limiting


ЁЯУж What's in this branch

Lessons 01тАУ10, plus "all or nothing" when the work spans several services, each with its own database. You learn why a local transaction is not enough, two-phase commit (2PC) and why it blocks, sagas (orchestration and choreography), compensations, idempotency, and the outbox from lesson 08 again.

ЁЯзТ Explain like I'm 5

Katrina books a school trip for her daughter. Three offices must agree: the seats office, the fees office and the bus office. Each office has its own book.

Plan A тАФ the phone call (2PC). Dipika phones all three: "Can you do it? If yes, hold it and wait for my word." All three say yes and hold: a seat is held, the money is held, the bus place is held. Then Dipika says "Go!" and all three write it down. But if Dipika's phone dies after "hold", the three offices wait. The seat stays held. The money stays held. Nobody else can use them. They wait until Dipika calls back.

Plan B тАФ the undo list (saga). Each office does its part at once and also writes down how to undo it. Seats: done (undo: release the seat). Fees: done (undo: refund). Bus: "Sorry, the bus is full." So the office walks back in reverse: refund the money, release the seat. Nobody waited. For a short time, the seat was taken and the money was charged тАФ and then both were undone.

ЁЯЧ║я╕П Diagram

flowchart LR
    subgraph tpc["ЁЯУЮ 2PC: one coordinator"]
      co["coordinator"] -->|"1 prepare"| s1["seats"]
      co -->|"1 prepare"| p1["fees"]
      co -->|"1 prepare"| b1["bus"]
      co -.->|"2 commit or abort"| s1
      dead["ЁЯТА coordinator dies after prepare<br/>тЖТ BLOCKED, locks held"]
    end
    subgraph sg["ЁЯУЛ saga: local steps + undo"]
      a["тЬУ reserve seat"] --> b["тЬУ charge тВ╣500"] --> c["тЬЧ book the bus"]
      c -->|"compensate"| rb["тЖй refund тВ╣500"] --> rs["тЖй release seat"]
    end

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/system-design/lesson-diagrams.html#l11

тЭУ What

ЁЯдФ Why

Because microservices split the data. The planning office must still promise "a trip is either booked fully or not at all". 2PC gives that promise, but it holds locks across services and stops when its coordinator stops. A saga never holds a global lock and keeps each service independent тАФ the price is a short in-between state that other readers can see (a seat taken, then released). So the office writes both: the steps and their undo, before building anything.

ЁЯФз How (in this repo)

In design/blocks.py:

transactions() in design/demo.py books a school trip.

ЁЯзк Try it

python3 design/demo.py transactions
python3 - <<'EOF'
import sys; sys.path.insert(0, "design"); from blocks import two_phase_commit, saga
for votes in ([True] * 3, [True, False, True], [False] * 3):
    print(votes, "тЖТ", two_phase_commit(votes), "| coordinator dead тЖТ", two_phase_commit(votes, coordinator_alive=False)[:7])
steps = [("reserve seat", "release seat"), ("charge Katrina тВ╣500", "refund Katrina тВ╣500"), ("book the bus", "cancel the bus")]
for f in (None, 0, 1, 2):
    s, log = saga(steps, fail_at=f)
    print(f"fail_at={f!s:<4} тЖТ {s:<11} {' ┬╖ '.join(log)}")
EOF

тЬЕ Verify тАФ what you should see

transactions prints:

тФАтФА a school-trip booking touches 3 services: seats, payment, bus
   2PC, all vote yes тЖТ COMMIT ┬╖ one votes no тЖТ ABORT
   2PC, the coordinator dies after 'prepare' тЖТ BLOCKED тАФ participants hold locks until the coordinator returns
   saga, the bus is full тЖТ COMPENSATED: тЬУ reserve seat ┬╖ тЬУ charge Katrina тВ╣500 ┬╖ тЬЧ book the bus ┬╖ тЖй refund Katrina тВ╣500 ┬╖ тЖй release seat
   sagas trade 'all at once' for 'eventually consistent, with an undo for every step'

Your snippet prints:

[True, True, True] тЖТ COMMIT | coordinator dead тЖТ BLOCKED
[True, False, True] тЖТ ABORT | coordinator dead тЖТ BLOCKED
[False, False, False] тЖТ ABORT | coordinator dead тЖТ BLOCKED
fail_at=None тЖТ DONE        тЬУ reserve seat ┬╖ тЬУ charge Katrina тВ╣500 ┬╖ тЬУ book the bus
fail_at=0    тЖТ COMPENSATED тЬЧ reserve seat
fail_at=1    тЖТ COMPENSATED тЬУ reserve seat ┬╖ тЬЧ charge Katrina тВ╣500 ┬╖ тЖй release seat
fail_at=2    тЖТ COMPENSATED тЬУ reserve seat ┬╖ тЬУ charge Katrina тВ╣500 ┬╖ тЬЧ book the bus ┬╖ тЖй refund Katrina тВ╣500 ┬╖ тЖй release seat

The tests include тЬЕ L11 2PC blocks without its coordinator; a saga compensates in reverse.

ЁЯПБ What you just proved

2PC is simple while the coordinator lives: all yes тЖТ COMMIT, one no тЖТ ABORT. When it dies after prepare, the answer is BLOCKED тАФ locks held, work stopped. The saga never blocks: a failure at step 0 needs no undo, a failure at step 1 undoes one step, a failure at step 2 undoes two, always newest first. The number of compensations grows with how far the saga got тАФ and each one must be safe to run twice.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ an orchestrated saga in AWS Step Functions. Each task has a Catch that jumps to the compensations, in reverse (Amazon States Language, shortened):

{
  "StartAt": "ReserveSeat",
  "States": {
    "ReserveSeat": { "Type": "Task", "Resource": "arn:aws:lambda:ap-south-1:111122223333:function:reserve-seat",
      "Catch": [{ "ErrorEquals": ["States.ALL"], "Next": "Failed" }], "Next": "ChargeFee" },
    "ChargeFee":   { "Type": "Task", "Resource": "arn:aws:lambda:ap-south-1:111122223333:function:charge-fee",
      "Catch": [{ "ErrorEquals": ["States.ALL"], "Next": "ReleaseSeat" }], "Next": "BookBus" },
    "BookBus":     { "Type": "Task", "Resource": "arn:aws:lambda:ap-south-1:111122223333:function:book-bus",
      "Catch": [{ "ErrorEquals": ["States.ALL"], "Next": "RefundFee" }], "End": true },
    "RefundFee":   { "Type": "Task", "Resource": "arn:aws:lambda:ap-south-1:111122223333:function:refund-fee",
      "Retry": [{ "ErrorEquals": ["States.ALL"], "MaxAttempts": 5, "BackoffRate": 2 }], "Next": "ReleaseSeat" },
    "ReleaseSeat": { "Type": "Task", "Resource": "arn:aws:lambda:ap-south-1:111122223333:function:release-seat",
      "Retry": [{ "ErrorEquals": ["States.ALL"], "MaxAttempts": 5, "BackoffRate": 2 }], "Next": "Failed" },
    "Failed":      { "Type": "Fail", "Error": "TripNotBooked" }
  }
}

Compensations retry (they must finish), so they must be idempotent. In each service, remember the keys you have already handled:

CREATE TABLE handled_requests (idempotency_key text PRIMARY KEY, result jsonb, created_at timestamptz DEFAULT now());
-- inside the same transaction as the refund:
INSERT INTO handled_requests (idempotency_key, result) VALUES ('trip-42-refund', '{"refunded": 500}')
ON CONFLICT (idempotency_key) DO NOTHING;   -- 0 rows inserted тЖТ it was done before; return the saved result

2PC does exist inside one database family. PostgreSQL has prepared transactions (off by default тАФ max_prepared_transactions = 0), used by transaction managers:

BEGIN; UPDATE seats SET taken = true WHERE trip = 42 AND seat = 17;
PREPARE TRANSACTION 'trip-42';     -- phase 1: saved to disk, locks held
COMMIT PREPARED 'trip-42';         -- phase 2 (or ROLLBACK PREPARED 'trip-42')

ЁЯПн Why this matters in production: a forgotten prepared transaction holds locks and stops VACUUM. Check SELECT * FROM pg_prepared_xacts;. For sagas, alarm on executions that end in the compensation path тАФ each one is a parent who saw a charge and a refund.

тПня╕П Next

The office can now agree and undo. But what if one parent's app sends 1,000 requests a second? Rate limiting.

git checkout lesson-12-rate-limiting
тЖР PreviousconsistencyNext тЖТrate limiting

This page is the lesson's README from the lesson-11-distributed-transactions branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.