Data Flow — reservation + payment saga
Full 5-phase flow: reserve → outbox fan-out → payment → finalize → expire/compensate.
CloudEvents on MSK, transactional outbox, DLQ policy. Source: 04-data-flow-sequence.mermaid
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#FF9900','primaryBorderColor':'#232F3E','primaryTextColor':'#232F3E','lineColor':'#232F3E','actorBkg':'#FF9900','actorBorder':'#232F3E','actorTextColor':'#232F3E','noteBkgColor':'#FDF6E3','noteTextColor':'#232F3E','noteBorderColor':'#232F3E','fontFamily':'Helvetica, Arial, sans-serif'}}}%%
sequenceDiagram
autonumber
actor U as Customer
participant K as kong-gateway
participant OS as order-service (Java)
participant TS as ticket-service (Go)
participant VS as venue-service (Go)
participant MQ as MSK Kafka
participant PS as payment-service (NestJS)
participant ST as Stripe
participant ES as expiration-service (Go)
participant RD as Redis timers
%% ----------- PHASE 1: Reserve --------------------------------
rect rgb(250,243,219)
Note over U,OS: Phase 1 — Reserve quota and seats (synchronous, gRPC)
U->>K: POST /orders (ticket_id, qty, optional seat_ids)
K->>OS: JWT verified — forward
OS->>TS: gRPC ReserveQuota(ticket_id, reservation_id, qty, expires_at +15m)
TS-->>OS: success, remaining, price, max_per_user
alt specific seats requested
OS->>VS: gRPC ReserveHeldSeats(plan_id, seat_ids)
VS-->>OS: success, seats[]
else auto-assign
OS->>VS: gRPC AutoAssignAndReserve(plan_id, section, qty)
VS-->>OS: success, seats[]
end
OS->>OS: INSERT orders (status=PENDING, expires_at) AND INSERT order_outbox
Note right of OS: Single DB txn (Spring @Transactional)
Transactional outbox pattern
OS-->>K: 201 Created (order_id, expires_at)
K-->>U: 201 Created
end
%% ----------- PHASE 2: Fan-out via Kafka ---------------------
rect rgb(234,242,251)
Note over OS,MQ: Phase 2 — Outbox drain to Kafka (CloudEvents envelope)
OS->>MQ: publish orders.order.created (key=order_id, partitioned by user_id)
par expiration timer scheduled
MQ-->>ES: consume orders.order.created
ES->>RD: ZADD timers fires_at reservation_id
and payment picks up
MQ-->>PS: consume orders.order.created
PS->>PS: INSERT payments (status=PENDING, idempotency=order_id)
end
end
%% ----------- PHASE 3: Payment --------------------------------
rect rgb(245,238,245)
Note over U,PS: Phase 3 — Client confirms payment
U->>K: POST /payments/confirm (order_id, payment_method)
K->>PS: JWT verified — forward
PS->>ST: create + confirm PaymentIntent (amount, currency)
ST-->>PS: status=succeeded (or requires_action)
PS->>PS: UPDATE payments SET status=SUCCEEDED AND INSERT payment_records
PS->>MQ: publish payments.payment.succeeded (CloudEvents)
ST-->>K: webhook payment_intent.succeeded (async, idempotent)
K->>PS: POST /payments/webhook
end
%% ----------- PHASE 4: Finalize ------------------------------
rect rgb(231,242,222)
Note over MQ,TS: Phase 4 — Finalize quota and complete order
MQ-->>OS: consume payments.payment.succeeded
OS->>TS: gRPC FinalizeReservation(reservation_id, order_id)
TS-->>OS: success
OS->>VS: gRPC FinalizeSeatReservation(reservation_id, order_id)
VS-->>OS: success
OS->>OS: UPDATE orders SET status=CONFIRMED AND outbox (orders.order.completed)
OS->>MQ: publish orders.order.completed
MQ-->>ES: consume orders.order.completed (cancel timer)
ES->>RD: ZREM timers reservation_id
end
%% ----------- PHASE 5: Expiry branch (compensation) ----------
rect rgb(253,238,238)
Note over ES,OS: Phase 5 — Expiry path (compensating saga)
ES->>RD: ZPOPMIN expired timers (every 1s)
ES->>MQ: publish expiration.order.expiration_complete
MQ-->>OS: consume expiration.order.expiration_complete
alt order still PENDING or AWAITING_PAYMENT
OS->>TS: gRPC ReleaseReservation(reservation_id, reason=EXPIRED)
OS->>VS: gRPC ReleaseSeatReservation(reservation_id, reason=EXPIRED)
OS->>OS: UPDATE orders SET status=EXPIRED
OS->>MQ: publish orders.order.cancelled
MQ-->>PS: consume orders.order.cancelled (void intent)
PS->>ST: cancel PaymentIntent (if not captured)
else already CONFIRMED
Note over OS: no-op (idempotent handler)
end
end
Note over MQ: Any consumer failure retries with exponential back-off (max 3),
then routes to topic.dlq — audited, never silently dropped.