Skip to content
SeeItFlow
System Design · Failure & Scale Scenarios · ~32 min read

Failure & Scale Scenarios

How each failure mode manifests, its user impact, how it's detected, how the system behaves safely in the moment, how it recovers, and how it's prevented -- from payment/hold races and expiry edge cases through messaging, ticket issuance, admission control, hot-event contention, reconciliation and regional failure.

Mangesh MandlikLast updated:
Scenario 01

Payment Timeout

The payment provider processes the charge, but the response is lost. The booking service does not know whether payment succeeded.

User impact

The booking stays PAYMENT_PENDING; the user doesn't yet see a confirmation, but nothing is charged twice and no seat is stolen.

Detection

The client retries with the same idempotency key, or reconciliation queries the provider for bookings that have stayed PAYMENT_PENDING past a threshold.

Safe behavior

Do not automatically retry as a brand-new payment and do not classify this as a decline -- it's unknown, not failed. Keep the booking pending while resolution is in flight.

Recovery

On confirmed success, transition to CONFIRMED if the hold still owns the seats (see Payment/Hold Boundary for the case where it doesn't). On confirmed failure, release the hold and return the seats to AVAILABLE.

Prevention

There is no hold duration that guarantees reconciliation always finishes before expiry -- see Architecture Deep Dive -> Payment/Hold Boundary. Design the late-success recovery path deliberately rather than assuming the TTL will always be long enough.

Scenario 02

Expiry vs. Payment Race

Payment resolves at nearly the same instant the hold reaches its expiry boundary -- both the confirmation path and the expiry worker are trying to transition the same hold.

Swipe to pan
Expiry vs. Payment RaceBoth paths target the same hold -- only a conditioned, versioned transition can decide safely.Payment Successconfirm holdExpiry Workerrelease holdHold Recordidentity + versionCONFIRMEDEXPIRED → re-list seatlate payment → recoveryCAS: version=vCAS: version=vonly one CAS winsIf expiry wins, the late payment success is routed to the dedicated recovery path -- never silently discarded or silently applied.
Payment confirmation and the expiry worker both race to transition the same hold -- a conditional, versioned compare-and-swap lets only one win, and a late-arriving payment after expiry is routed to a dedicated recovery path rather than silently applied or discarded.

User impact

Without a precise guard, the seat could be confirmed and released at the same time -- an internally inconsistent state that risks either an oversell or wrongly losing a paid seat.

Detection

A conditional, versioned transition on the hold record rejects one of the two concurrent attempts.

Safe behavior

Confirmation must validate booking/hold identity, payment state, current hold ownership, and the hold's expiry/version condition together -- not merely 'is the seat still marked HELD.' Expiry must equally validate the specific hold's identity/version, not just the seat's current state (see Expiry Generation Protection). Whichever conditional transition commits first wins; the other's conditional check fails cleanly.

Recovery

If expiry wins, the late-arriving payment success falls through to the Payment Success After Hold Expiry recovery path below rather than being silently applied.

Prevention

Never implement either side as a bare state check -- both must be scoped to the specific hold's identity and version.

Scenario 03

Booking Database Failure

The booking database becomes unavailable at various points in the reservation/confirmation sequence.

User impact

During reservation: the hold request fails cleanly with no hold created. During confirmation: the booking may sit unresolved until the database recovers.

Detection

Connection errors/timeouts from the booking-write path.

Safe behavior

Reject the hold request cleanly rather than guess -- never create a hold that can't be durably persisted. If the client experiences a lost response after the DB actually committed, its retry (carrying the same idempotency key) must discover and reuse the existing hold rather than blindly allocating a second one.

Recovery

Once the database recovers, in-flight PAYMENT_PENDING bookings are picked up by reconciliation and resolved against actual payment state.

Prevention

Never optimistically confirm a booking without database durability -- a delayed confirmation is recoverable; a confirmation that was never durably recorded is not.

Scenario 04

Stale Availability

The seat map a user sees can be stale in two different directions -- both are UX costs, not correctness failures.

User impact (stale positive)

A seat shown as AVAILABLE was actually just held or confirmed by someone else. The user's reservation attempt is correctly rejected by the authoritative path, and they're shown a fresh view.

User impact (stale negative)

A seat that just became available again (its hold expired) still shows as unavailable in the cached view until the read model catches up -- a missed-opportunity UX cost, not an allocation error.

Detection

A reservation attempt against a stale-positive seat fails its conditional update; stale-negative cases surface as availability-freshness lag metrics.

Safe behavior

The reservation path never trusts the cache -- it always re-validates against authoritative state. Neither staleness direction can cause a double allocation as long as this holds.

Recovery

The UI refreshes the seat map after a rejected reservation attempt so the user sees corrected state for their next action.

Prevention

Choose a cache/invalidation strategy against a stated freshness SLO -- see Trade-offs & Decisions -> Availability Cache Strategy -- rather than assuming a single fixed TTL fits every event.

Scenario 05

High-Demand Event

An illustrative scenario: a very large number of requests arrive for a small number of seats within seconds of on-sale. Almost everyone will fail to get a seat -- the system must fail them correctly and stay healthy while doing so.

User impact

The vast majority of users don't get a seat -- that's an expected outcome of scarce inventory, not a system failure, as long as the ones who do succeed get a correct, non-duplicated booking.

Detection

Read traffic spike on the availability path; reservation-attempt volume spike on the booking path; queue/admission-controller backlog growth.

Safe behavior

Read traffic is absorbed by the availability cache/read model. Reservation requests pass through admission control sized to the slowest correctness-critical dependency (see Trade-offs & Decisions -> Reject vs. Admit), not simply application-server count.

Recovery

As the event sells out or demand normalizes, admission and reservation load return to baseline.

Prevention

Any specific traffic number used to illustrate this scenario is an example, not a universal capacity fact -- size real capacity against measured load tests for the specific event and infrastructure in question.

Scenario 06

Duplicate Hold Request

A client double-clicks Reserve, or retries after a perceived timeout.

User impact

Without protection: a second, independent hold on different or overlapping seats.

Detection

The retried request carries the same idempotency key as the original.

Safe behavior

Resolve the duplicate against the durable idempotency record and return the same logical hold -- no second reservation attempt.

Recovery

No repair needed -- this is the expected, correct behavior of an idempotent hold path.

Prevention

See Architecture Deep Dive -> Hold Idempotency for the durable key and request-fingerprint mechanism.

Scenario 07

Hold Created, Response Lost

The database commits the hold, but the response never reaches the client.

User impact

The user sees no confirmation and may retry, believing nothing happened.

Detection

The client's retry carries the same idempotency key used in the original request.

Safe behavior

The retry must discover and reuse the existing hold rather than allocate a second one -- exactly the idempotency mechanism above.

Recovery

The retry returns the existing hold's details (seats, expires_at) as if it were the original response.

Prevention

Always resolve retries against the durable idempotency record before considering a request 'new.'

Scenario 08

Old Expiry Task Releases a Newer Hold

The critical expiry correctness scenario: a delayed cleanup task for an already-expired hold runs after a different user has already acquired the same seat.

User impact

Without protection, User B's legitimately-acquired new hold (or already-confirmed booking) is incorrectly released, potentially causing a lost sale or -- worse -- opening the seat to a third party while B still believes they hold it.

Detection

The delayed task's conditional release check includes the original hold's identity (hold_id) and finds it no longer matches the seat's current holder.

Safe behavior

The release condition is scoped to the specific hold identity and version, not just seat state -- WHERE hold_id = :old_hold_id AND state = 'HELD' AND held_until <= now. Because the seat's current hold_id is now B's, this condition matches zero rows and does nothing.

Recovery

No repair needed if the identity-scoped condition was in place from the start -- this is exactly the failure the mechanism exists to prevent.

Prevention

See Architecture Deep Dive -> Expiry Generation Protection -- this is the single most important safeguard in the hold-expiry design.

Scenario 09

Expiry Worker Down

The expiry worker (or its infrastructure) is unavailable for a period of time. Expired holds accumulate without being cleaned up.

User impact

Seats whose holds have business-expired may still appear held/unavailable in the read model, even though no one is actually paying for them anymore.

Detection

Expiry lag (now - expires_at for holds still ACTIVE) grows and breaches its alert threshold; count of active holds past expiry rises.

Safe behavior

Reservation and confirmation logic must still treat an expired hold as invalid based on authoritative time semantics -- expires_at defines business validity immediately, independent of when a cleanup worker physically gets around to updating the row.

Recovery

Once the worker recovers, it processes the backlog in bounded batches rather than one giant sweep.

Prevention

Alert on sustained expiry lag well before it becomes user-visible; run the worker with enough redundancy that a single instance failing doesn't halt expiry entirely.

Scenario 10

Multiple Expiry Workers Running Concurrently

More than one expiry worker instance processes the same expired hold at nearly the same time.

User impact

None, if release is correctly idempotent -- both workers attempt the same conditional release, only one succeeds, the other's update affects zero rows.

Detection

Duplicate release attempts visible in logs/metrics as zero-row-affected conditional updates -- expected noise, not an error.

Safe behavior

The identity-scoped conditional release (see Expiry Generation Protection) makes a duplicate attempt a safe no-op by construction.

Recovery

None needed.

Prevention

Partition expiry work across workers (by event, by hold-ID range, or a claim/skip-locked pattern) to reduce wasted duplicate work, while still relying on the conditional check as the actual correctness guarantee.

Scenario 11

Expiry Worker Overloads the Database

An expiry worker's scan or release operations compete with live reservation traffic for database capacity, especially at event scale with many concurrent active holds.

User impact

Elevated latency or contention on the reservation path caused by the expiry worker's own load, ironically during the highest-demand moments when timely expiry matters most.

Detection

Database load/latency correlated with expiry worker scan cycles.

Safe behavior

Use an index on (state, expires_at), bounded batch sizes, and throttled/partitioned work rather than one large unbounded query -- see Architecture Deep Dive -> Expiry Worker Design.

Recovery

Reduce batch size or worker concurrency temporarily if contention is observed; let the backlog drain at a sustainable rate rather than forcing it.

Prevention

Load-test the expiry worker against a realistic number of concurrent active holds for a blockbuster event before it happens for real.

Scenario 12

Multi-Seat Partial Conflict

A user requests A1, A2, A3. A2 is already held by someone else.

User impact

If the product promises all-or-none and the system silently reserves A1 and A3 while failing A2, the user ends up with a partial, unwanted booking they didn't ask for.

Detection

The multi-seat transaction's verification step finds A2 already unavailable before committing.

Safe behavior

Enforce the all-or-none contract explicitly: if any requested seat can't be acquired and partial holds aren't an explicit supported feature, roll back the entire attempt and report which seat(s) conflicted.

Recovery

The user is shown the conflict and can retry with a different seat selection.

Prevention

See Architecture Deep Dive -> Multi-Seat Reservation -- verify all requested seats inside one transaction before committing any of them.

Scenario 13

Multi-Seat Deadlock

Two concurrent multi-seat requests lock overlapping seat sets in different orders -- User A locks A1 then tries A2 while User B locks A2 then tries A1.

User impact

Both transactions block waiting on each other, eventually timing out or being deadlock-aborted by the database -- a failed request for one or both users.

Detection

Database deadlock detection aborts one of the two transactions.

Safe behavior

Always acquire seats within a multi-seat transaction in a deterministic order (e.g. sorted by seat_id), regardless of the order the user selected them in -- this prevents the circular-wait condition that causes deadlocks.

Recovery

The aborted transaction retries the whole multi-seat attempt from the start, now free of the ordering conflict.

Prevention

See Architecture Deep Dive -> Multi-Seat Reservation -- deterministic lock ordering is the standard fix for this class of deadlock.

Scenario 14

Payment Success After Hold Expiry (Seat Not Yet Reallocated)

The hold expires, but no one else has taken the seat yet, when payment resolves to success.

User impact

The customer paid successfully and reasonably expects their seat -- but the hold that backed the reservation has technically expired.

Detection

Reconciliation or the payment webhook resolves success; the confirmation step checks current hold/seat ownership and finds the hold no longer ACTIVE.

Safe behavior

This is a product policy decision, not a universal rule: the system may attempt a conditional reacquisition/confirmation (if the seat is still unallocated to anyone else) or fall back to refund/void. Do not silently confirm against an expired hold as if nothing happened.

Recovery

If reacquisition succeeds, confirm the booking normally. If policy chooses refund/void instead, initiate that and notify the customer clearly about what happened and why.

Prevention

Define this policy explicitly ahead of time -- see Architecture Deep Dive -> Payment/Hold Boundary -- rather than deciding ad hoc during an incident.

Scenario 15

Payment Success, Seat Already Reallocated

The hold expired, the seat was reallocated to a different booking, and only then does the original payment resolve as successful. This is the scenario that must never result in double-booking.

User impact

The original customer paid but cannot receive that specific seat -- someone else legitimately holds it now.

Detection

Confirmation's ownership check finds the seat now belongs to a different, valid hold or confirmed booking.

Safe behavior

Never steal the seat back from the party who now legitimately owns it. Preserve the other booking exactly as-is.

Recovery

Void or refund the original late-successful payment, mark the original booking as an exception/reconciliation case, and notify the original customer according to product policy (offer an alternate seat, a refund, or both).

Prevention

This scenario is why the payment/hold boundary policy has to exist deliberately -- see Architecture Deep Dive -> Payment/Hold Boundary -- rather than assuming it will never happen.

Scenario 16

Payment Webhook Duplicate

The payment provider delivers the same webhook event more than once -- expected under at-least-once delivery.

User impact

None, if deduplicated correctly -- if not, a duplicate confirmation attempt or duplicate side effect could occur.

Detection

Deduplicate on the provider's event/delivery ID.

Safe behavior

The second and later deliveries are recognized as already-processed and acknowledged without reapplying the business effect.

Recovery

None needed -- this is expected steady-state behavior.

Prevention

See the Payment System module for the full webhook-deduplication mechanics; this module's booking side just needs to trust that the payment integration tolerates duplicate events by design.

Scenario 17

Payment Webhook Out of Order

An older payment status event is delivered after a newer, terminal one has already been applied.

User impact

Applying the stale event naively could regress a successfully confirmed booking's payment status back to pending or failed.

Detection

A guarded transition check on payment/booking state rejects the invalid regression.

Safe behavior

Never let a terminal payment/booking state be regressed by a late-arriving, out-of-order event -- validate every incoming event against the current state's valid-transition table before applying it.

Recovery

The rejected event is logged for observability; no state change occurs.

Prevention

Apply the same guarded-transition discipline used elsewhere in the booking state machine (see Architecture Deep Dive -> Booking State Machine) to payment events specifically.

Scenario 18

Booking Confirmed, Event Publish Fails

The booking database commits CONFIRMED, but the direct publish call to the broker fails or the process crashes before it happens.

User impact

Without an outbox, ticket issuance and notification never fire -- the customer paid and is confirmed in the database, but receives no ticket.

Detection

With a transactional outbox: outbox-lag monitoring surfaces unpublished rows directly. Without one: a customer complaint about a missing ticket.

Safe behavior

Use a transactional outbox so the event's existence is guaranteed by the same transaction that committed CONFIRMED -- the gap becomes a bounded relay-lag problem to monitor, not a silent loss.

Recovery

The outbox relay retries publishing the unpublished row until the broker acknowledges it.

Prevention

See Architecture Deep Dive -> Transactional Outbox and Trade-offs & Decisions -> Direct Publish vs. Outbox.

Scenario 19

Duplicate booking.confirmed Event

The outbox relay (or any at-least-once publisher) delivers the same booking.confirmed event to a consumer more than once.

User impact

If ticket issuance isn't idempotent: a second, independently valid ticket for the same seat. If notification isn't idempotent: a duplicate confirmation email -- a smaller, but still real, UX cost.

Detection

The event carries a stable event/booking/business-operation identity the consumer checks against its own already-processed record.

Safe behavior

Each consumer deduplicates or makes its own operation idempotent using that stable identity before applying its effect.

Recovery

No system-level recovery needed -- this is expected steady-state behavior of a correctly built consumer.

Prevention

See Architecture Deep Dive -> Ticket Issuance -- every downstream consumer needs this discipline, not only the ticket generator.

Scenario 20

Ticket Generation Failure

The ticket-issuance consumer fails to generate the ticket artifact after receiving booking.confirmed.

User impact

The customer's booking is confirmed and paid, but no ticket/entitlement is immediately available.

Detection

Ticket issuance lag metric, or a confirmed booking with no corresponding ticket record past an expected window.

Safe behavior

The booking remains CONFIRMED regardless -- ticket generation failure does not roll back a valid, paid booking.

Recovery

Retry ticket issuance asynchronously; the customer can retrieve their ticket once issuance succeeds, without needing to redo checkout.

Prevention

Monitor ticket issuance lag as its own metric, separate from booking confirmation latency -- see Architecture Deep Dive -> Observability.

Scenario 21

Duplicate Ticket Generation

A retry or duplicate event causes ticket issuance to run twice for the same confirmed seat entitlement.

User impact

Without protection: two independently usable tickets exist for one paid seat -- a real risk if the product involves admission scanning (two people could both show up with a 'valid' ticket).

Detection

A stable business idempotency key (e.g. UNIQUE(booking_id, seat_id)) on the ticket record detects the duplicate attempt.

Safe behavior

The second issuance attempt recognizes the existing ticket and returns/no-ops rather than minting a new one.

Recovery

No repair needed if the constraint held from the start; if a duplicate somehow already exists, invalidate the extra one and audit how it was created.

Prevention

See Architecture Deep Dive -> Ticket Issuance -- enforce the uniqueness constraint at the database level, not only in application logic.

Scenario 22

Availability Invalidation Lost

A cache-invalidation message for a seat-state change is dropped or never delivered.

User impact

The seat map shows outdated availability until the entry's TTL expires or the read model otherwise refreshes -- a display-freshness issue only.

Detection

Availability-freshness-lag metric, or a user report of a seat map that doesn't match reality.

Safe behavior

TTL expiry (or a periodic read-model rebuild) is the backstop that bounds how long the staleness can persist, independent of whether any single invalidation message was delivered. Reservation correctness is entirely unaffected -- the reservation path never depended on the invalidation having arrived.

Recovery

The cache entry naturally refreshes on its next TTL expiry or rebuild cycle.

Prevention

Always pair invalidation-based freshness with a TTL backstop rather than relying on invalidation delivery alone.

Scenario 23

Availability Cache Outage

The availability cache becomes fully unavailable, and all seat-map reads would otherwise fall through to the booking database at once.

User impact

Seat-map browsing slows down or degrades; reservation and confirmation are unaffected, since they never depended on this cache.

Detection

Cache health checks/connection errors; a spike in booking-DB read traffic.

Safe behavior

Do not blindly send all seat-map traffic to the booking database. Apply rate limiting, serve stale snapshots where acceptable, reduce refresh frequency, and shed nonessential read load to protect the database that the correctness-critical reservation path also depends on.

Recovery

Once the cache recovers, read traffic returns to its normal path and DB load drops back down.

Prevention

Load-test the booking database against a simulated full-cache-miss burst ahead of a known high-demand on-sale.

Scenario 24

Waiting Room Outage

The virtual waiting room infrastructure becomes unavailable while a high-demand on-sale is in progress.

User impact

Depends on the defined policy: users already admitted may or may not be able to continue, and new admission may need to pause entirely.

Detection

Waiting-room health checks; admission-rate metric drops to zero unexpectedly.

Safe behavior

Define explicitly, ahead of time, whether already-issued admission tokens remain valid during the outage and whether new admission pauses -- do not leave this ambiguous and decide ad hoc during the incident.

Recovery

Once the waiting room recovers, admission resumes at its configured rate.

Prevention

Run the waiting room with enough redundancy that it doesn't become a single point of failure for the entire on-sale.

Scenario 25

Admission Token Replay

A user attempts to reuse an admission token multiple times, or after it has expired.

User impact

None, if the token and booking path are both correctly bounded -- a replayed or expired token simply doesn't grant anything extra.

Detection

The token's own expiry/scope check fails, or booking idempotency recognizes a request as already handled.

Safe behavior

The token should be bounded and scoped (time-limited, tied to a specific admission event); booking idempotency still applies fully underneath it -- a token replay does not bypass the reservation invariants.

Recovery

An expired token requires re-admission through the waiting room, not a silent re-grant.

Prevention

Never treat admission-token possession as equivalent to a reservation guarantee -- see Architecture Deep Dive -> Virtual Waiting Room.

Scenario 26

Hot-Event Partition Saturation

One blockbuster event drives so much load that its shard/partition saturates, even while the rest of the platform is healthy.

User impact

Elevated latency and reservation-failure rate specifically for that event, disproportionate to overall platform traffic.

Detection

Per-event/per-partition load metrics spike well beyond the platform average.

Safe behavior

Event-level sharding alone does not solve this -- the whole event lands on one shard. Finer partitioning (event -> section/block -> seat) and admission control specifically for that event are the responses -- see Architecture Deep Dive -> High-Demand Partitioning.

Recovery

Load subsides once the event sells out or demand normalizes; sold-out detection (see below) should stop unnecessary further load quickly.

Prevention

For known high-demand on-sales, apply finer partitioning and admission control proactively rather than discovering the hot-shard bottleneck live.

Scenario 27

Hot-Seat Contention

Many users converge on the same especially attractive seat (front row, best view) at the same instant.

User impact

All but one of the concurrent requests for that seat fail -- expected, since only one can win a unique seat -- but a poorly designed client could make this worse by retrying uselessly.

Detection

Reservation conflict/failure-rate metric spikes for one specific seat, disproportionate to overall traffic.

Safe behavior

Fail the losing requests quickly and clearly -- do not have clients blindly retry the exact same seat after a legitimate ownership conflict (see Trade-offs & Decisions -> Locking vs. Optimistic Concurrency). Point the user toward other available seats instead.

Recovery

Contention subsides once the seat is HELD/CONFIRMED and the read model reflects that.

Prevention

Design client UX to avoid encouraging rapid re-clicks on a seat that just failed -- surface the conflict clearly and suggest alternatives.

Scenario 28

Sold-Out Transition

Authoritative inventory for an event reaches zero available seats.

User impact

Without a fast sold-out signal, users continue attempting reservations and payments that are doomed to fail, wasting their own time and system capacity.

Detection

Available-seat count for the event reaches zero, accounting for currently active holds that may still expire and free up inventory.

Safe behavior

Stop unnecessary admission/payment work promptly once inventory is genuinely exhausted -- while still correctly accounting for active holds that could still expire and briefly reopen availability.

Recovery

If a hold expires and frees a seat after a sold-out signal was shown, the availability read model should reflect that reopening once it refreshes.

Prevention

Distinguish 'zero seats available right now' from 'zero seats and zero active holds' when deciding whether to show a hard sold-out message or a 'seats may become available' message.

Scenario 29

Hold Storm

An unusually large number of holds are created in a short window, but relatively few convert to actual payments -- the event can appear sold out even though most of those seats will free up shortly.

User impact

Users see the event as unavailable, become frustrated, and may abandon -- even though a wave of holds is about to expire and free up real inventory.

Detection

A conversion-rate metric (holds created vs. holds confirmed) drops sharply while active-hold count spikes.

Safe behavior

Track hold-to-confirmation conversion as a first-class metric, not just raw hold counts. Admission policy can factor in expected conversion rate rather than treating every hold as equally likely to convert.

Recovery

As holds expire, inventory frees up and becomes available again -- the system should reflect that promptly rather than continuing to look artificially sold out.

Prevention

Consider admission or hold-policy adjustments (e.g. tighter admission during a known low-conversion pattern) if hold storms are a recurring problem for a given event type.

Scenario 30

Payment Provider Outage

The payment provider becomes unavailable while holds continue to be created.

User impact

Users can select and hold seats but cannot complete payment -- if unaddressed, this locks inventory that can't actually convert to a sale.

Detection

Payment provider error/timeout rate spikes.

Safe behavior

Pause or limit new holds, or adjust hold policy, rather than allowing inventory to remain locked indefinitely by holds that have no realistic path to payment during the outage.

Recovery

Once the provider recovers, resume normal hold/payment flow; any holds that expired during the outage return their seats to availability normally.

Prevention

Monitor payment provider health as an input to admission/hold policy, not just as a per-request retry concern.

Scenario 31

Reconciliation Backlog

Payment/booking ambiguity accumulates faster than reconciliation can resolve it.

User impact

Customers with ambiguous bookings wait longer than expected to learn whether their seat is confirmed.

Detection

Oldest-pending-age metric and mismatch-count metric both trend upward.

Safe behavior

Alert on both the oldest pending case's age and the overall mismatch count -- a growing backlog is itself a signal, separate from any single case's resolution.

Recovery

Scale reconciliation worker capacity, or triage by oldest/highest-impact cases first, until the backlog drains.

Prevention

Treat reconciliation capacity as a first-class scaling concern, not an afterthought batch job -- see Architecture Deep Dive -> Booking Reconciliation.

Scenario 32

Cancellation vs. Ticket Use

A booking is cancelled after its ticket has already been issued -- and, for admission-scanning use cases, potentially after the ticket has already been used to gain entry.

User impact

Without a defined interaction, a cancelled booking's ticket could still scan as valid at the venue, or a used ticket could still show as active in the customer's app.

Detection

Cancellation request against a booking whose ticket has already been issued (or scanned, if the domain tracks that).

Safe behavior

Where the product requires admission scanning, cancellation must invalidate/revoke the issued ticket as part of the same workflow -- not leave it independently valid.

Recovery

If a ticket was already used before cancellation was processed, that's a policy exception to handle explicitly (e.g. no refund, or a manual review case) rather than a silent gap.

Prevention

See Architecture Deep Dive -> Cancellation & Refund -- ticket revocation is part of the cancellation workflow whenever ticket validity is checked at the point of use.

Scenario 33

Regional Failure

A region hosting part of the booking platform becomes unavailable -- the hardest scenario here, since two regions must never independently believe they each own the same scarce seat.

User impact

In-flight holds/bookings during the failover window carry the same ambiguity risk as any other unresolved attempt, compounded by the question of which region has write authority for a given seat.

Detection

Regional health checks and failover automation; defined RPO/RTO targets for the booking database.

Safe behavior

Define, ahead of any failure, which region owns write authority for a given event/partition's seats, and ensure fencing prevents the original region from resuming writes unexpectedly after it recovers.

Recovery

Failover promotes a standby region using the same fencing discipline as any single-leader failover; in-flight holds/bookings are resolved via the same reconciliation mechanisms used for any other unresolved case.

Prevention

Treat active-active multi-region scarce-seat reservation as an advanced extension requiring deliberate fencing design, not a default -- see Trade-offs & Decisions -> Single-Region Owner vs. Multi-Region Active-Active.

Seat ownership across the failover

Ownership is per event or partition, so every seat has exactly one region that may allocate it. A hold's expires_at is an absolute timestamp, so a hold that lapsed during the failover window is already invalid when the new owner starts serving; it does not get a fresh TTL because the region changed. Reservation and confirmation still validate hold identity and version, so a late write from the old region, or an expiry task that outlived its hold, cannot release or confirm a seat that now belongs to a different hold. Payments left unresolved by the window follow the existing payment-after-expiry policy rather than a failover-specific one.

Triage by scenario family

FamilyScenariosCore response
Payment and confirmation01, 02, 14, 15, 16, 17, 30, 31Pending, never failed, on timeout; validate hold identity and expiry with payment state; explicit late-payment policy; never steal a reallocated seat; dedupe webhooks and reject regressions; reconcile
Hold creation and idempotency03, 06, 07, 12, 13Reject cleanly when the DB is down; durable idempotency record reuses the existing hold; multi-seat is all-or-none in deterministic seat order
Hold expiry02, 08, 09, 10, 11Release scoped to hold identity and version; expires_at defines validity without the worker; indexed, bounded batches
Availability and read path04, 22, 23Reservation never trusts the cache; TTL or rebuild bounds staleness; rate-limit and serve stale snapshots on cache outage
Demand and admission05, 24, 25, 26, 27, 28, 29Admission sized to the slowest correctness-critical dependency; bounded, scoped tokens with idempotency underneath; finer partitioning for a hot event; fail losing requests fast; track hold-to-confirmation conversion
Events and tickets18, 19, 20, 21, 32Outbox plus consumer idempotency; booking stays CONFIRMED if ticket generation fails; revoke tickets on cancellation
Region33Per-event/partition write owner and fencing
Key takeaways
  1. 01Anything money-adjacent starts from one rule: unknown is not failed. Keep the booking pending, resolve through status query, webhook or reconciliation, and never create a second independent attempt.
  2. 02Hold and expiry safety come from identity: hold_id plus version in every conditional release or confirmation, with expires_at as the business truth, so duplicate, delayed or absent workers are harmless.
  3. 03The reservation path never trusts the cache; stale availability costs conversion, not correctness.
  4. 04Size admission to the slowest correctness-critical dependency, and treat a blockbuster event as a partitioning problem, since event-level sharding alone leaves it on one shard.
  5. 05Terminal states and other owners are never overridden: a late payment cannot take a reallocated seat, and an out-of-order event cannot regress a terminal state.
  6. 06Region failover needs predefined per-event/partition seat ownership and fencing; multi-region active-active reservation is an advanced extension, not a default.