React Forum
Blog & Guide Responses
Feb 07, 2023 · 04:00 PM

[Guide] Build a Ticket Booking App (Idempotent "Reserve") - implementation notes

Started by Lena Fischer
guidebookingticketsidempotent reserveroute stepsderived totalslanesevidence

The Ticket Booking guide frames booking as a state machine where the most important action (reserve) is safe to run repeatedly. Pair that with derived totals stored for stability, and route steps for navigation without URL churn. It's a strong posture: correctness comes from idempotence + explicit lanes, not from hoping users click once.

Read the guide

How did you model idempotent reserve so retries and double-clicks are safe and explainable? Did you store derived totals (fees, counts) and how did you keep them correct under partial failures? How did you implement route steps (seat picker -> summary -> payment) so back/forward replays the same contract? What evidence did you render so support can see which step/lane/intent the user is in from a screenshot?

Comments (22)

Back to latest
Marisol Vega
Feb 10, 2023 · 10:10 AM

Idempotent reserve is the only way booking UIs stay sane under real user behavior.

People refresh, double-click, lose connection, and come back. Your model has to accept that.

Dmitri Kline
Feb 14, 2023 · 02:14 PM

We treated reserve as an intent with a stable idempotency key, and we logged it like a contract transition:

txt
[tips] intent=reserve key=rsv_883 seats=2 lane=pending reason=user:click
[tips] intent=reserve key=rsv_883 seats=2 lane=ok reason=server:confirm holdId=h_19
[tips] intent=reserve key=rsv_883 seats=2 lane=ok reason=retry:doubleClick dedup=true

The third line is the whole point. It turns "double click" from a bug into a normal story.

Keira Santos
Feb 18, 2023 · 06:06 AM

Counterpoint: idempotence can hide UX problems if you rely on it too much.

We still disabled the button while pending and showed a lane chip. Idempotence is correctness; UX still matters.

Lena Fischer
Feb 21, 2023 · 04:04 AM

Yes. Idempotence is insurance, not an excuse.

The UI should still guide the user into the happy path, and evidence should explain what happened when they stray.

Grace McCoy
Mar 01, 2023 · 03:03 AM

Derived totals stored were necessary because the seat picker and summary both need the same numbers.

If they compute independently, you get the trust-killer: totals change between steps with no explanation.

Jasper Nguyen
Mar 14, 2023 · 02:14 PM

Long-form: booking failures are rarely pure errors; they're partial success with timing.

Seat held but payment failed. Payment pending but confirmation delayed. Idempotent reserve makes those partial states representable.

The UI should render lanes for each phase (hold, payment, confirm) and the logs should match.

Nina Kapoor
Mar 29, 2023 · 03:03 AM

Route steps were great because they let back/forward replay the stepper contract without forcing new URLs. We log step transitions so they aren't mysterious:

txt
[tips] step=seats -> step=summary reason=user:next
[tips] step=summary -> step=payment reason=user:next
[tips] step=payment -> step=summary reason=back
Camille Duarte
Apr 18, 2023 · 06:18 PM

We rendered evidence for step + lane + idempotency key so screenshots were actionable.

It printed in debug mode like step=payment reserveLane=pending reserveKey=rsv_883.

Owen Price
May 10, 2023 · 10:10 AM

Counterpoint: storing derived totals can become stale if you forget to re-derive after a seat change or fee update.

We enforced a single mutation helper that always triggers derive, and we log derive reasons.

Benji Rios
Jun 01, 2023 · 06:06 AM

We treated reserve as safe-to-repeat, but we still needed a story for race conditions (two tabs). We logged conflicts explicitly so support didn't have to guess:

txt
[tips] intent=reserve conflict key=rsv_883 winner=tab:A loser=tab:B reason=server:dedup
Lina Ortega
Jun 25, 2023 · 10:22 PM

Docs tie-in: idempotent actions are a core concept story (repeatability) more than a networking story.

Related: Core Concepts (Best Practices) and State Management (Best Practices).

Amina Farouk
Jul 21, 2023 · 07:07 AM

We also stored derived "reservation story" as a string that includes last intent and lane.

It sounds cheesy, but it made support tickets readable.

Evan Brooks
Aug 19, 2023 · 07:19 PM

Long-form counterpoint: idempotence can make teams complacent about monitoring because failures are "safe".

But safe failures are still failures, and users still feel them. We logged lane durations and treated long pending as a product problem.

Harper Iqbal
Sep 18, 2023 · 12:12 PM

We also had to make sure reserve key generation was deterministic per selection, not per click.

Otherwise you end up with many keys for the same intent and dedup can't work.

Priya Liao
Oct 15, 2023 · 03:15 PM

Counterpoint: sometimes the best UX is to let users change seats without tearing down the whole reserve flow.

We treated seat changes as a new reserve intent but kept the stepper stable so it didn't feel like a reset.

Theo Bennett
Nov 12, 2023 · 04:04 AM

Idempotent reserve also made it easier to write tests: tests can click twice and assert nothing breaks.

That seems trivial, but it's a strong signal that your model matches user reality.

Tomas Rivera
Dec 02, 2023 · 02:02 AM

We treated payment as a separate lane that can fail without invalidating the hold lane.

Separating lanes prevented the classic bug: payment failure resets seat selection silently.

Salma Qureshi
Dec 12, 2023 · 12:12 PM

Long-form: this guide is basically "make booking observable".

If a user says "I clicked reserve", the app should be able to say what intent it recorded, what lane it's in, and what it will do next. That's the contract.

Rowan Pierce
Dec 20, 2023 · 06:18 PM

We also derived totals in one place and used them everywhere, including confirmation emails.

If emails and UI disagree, trust is destroyed. Derived totals as contract prevented that drift.

Soren Malik
Dec 23, 2023 · 11:23 PM

Counterpoint: seat pickers are inherently interactive; trying to force them into a rigid stepper can feel clunky.

We allowed some step skipping but still kept the lane/evidence story consistent.

Mina Calder
Dec 29, 2023 · 09:09 AM

We also logged "reserve replay" on refresh so it's clear why the UI restored a pending state:

txt
[tips] replay intent=reserve key=rsv_883 lane=pending reason=refresh
Jules Kwon
Dec 30, 2023 · 10:10 AM

If you implement this, start by writing your lanes down (hold, payment, confirm) and decide what evidence each lane produces.

Then code it. Otherwise the booking flow becomes a pile of special cases.

Ari Novak
Dec 31, 2023 · 09:09 AM

The idempotent framing made the app feel professional.

Users do weird things; professional systems treat weird things as normal paths, not as exceptions.