---
title: "Idempotency and Duplicate Booking Prevention"
description: "Design idempotency boundaries and duplicate-booking safeguards across travel booking, payment, cancellation and refund operations."
slug: "idempotency-duplicate-booking-prevention"
translationKey: "architecture-idempotency-duplicate-booking-prevention"
locale: "en"
type: "guide"
category: "architecture"
tags: ["idempotency","duplicate-booking","retry","booking","travel"]
publishedAt: "2026-09-27"
updatedAt: "2026-09-27"
reviewedAt: "2026-09-27"
technicalVerifiedAt: "2026-09-27"
codeExampleStatus: "illustrative"
---

Retry is not inherently safe in travel booking. Without an **idempotency boundary that prevents the same business intent from being applied twice**, recovery can create duplicate reservations or double charges.

## Where idempotency belongs

Use separate keys for booking create, payment authorization, capture, modification, cancellation and refund. Never reuse one operation key for another semantic operation.

## Canonical key

```text
bookingIntentId + operation + version
```

Example:

```text
book:bi_123:v1
capture:bi_123:v1
cancel:bk_987:v1
refund:pay_456:v1
```

Keys should be deterministic and contain no PII.

## Internal idempotency store

```json
{
  "key": "book:bi_123:v1",
  "operation": "BOOK",
  "status": "IN_PROGRESS",
  "requestHash": "sha256:...",
  "resultReference": null,
  "expiresAt": "2026-09-28T10:00:00Z"
}
```

The same key with a different payload should be treated as a conflict.

## Duplicate prevention flow

```mermaid
%% title: Duplicate booking prevention
%% description: Booking create checks the idempotency store before any new provider request is started.
flowchart LR
  A[Create Booking Request] --> B[Idempotency Lookup]
  B -->|new| C[Reserve Key]
  B -->|existing| D[Return Existing State]
  C --> E[Provider Create]
  E --> F[Persist Result]
  F --> G[Return Booking State]
```

## When the provider supports idempotency

Persist the mapping between internal operation key and provider idempotency key. Provider support does not remove the need for internal deduplication because client retry and provider retry are different layers.

## When the provider does not support idempotency

Use stricter guards: one active create attempt per BookingIntent, persistent uniqueness, no create retry while UNKNOWN, provider lookup before retry and audited manual overrides.

## Database guard

A useful final safeguard is:

```text
UNIQUE (booking_intent_id, operation, operation_version)
```

A distributed lock alone is weaker because locks can expire; persistent uniqueness remains authoritative.

## Request hash

Prevent a caller from reusing one key for different semantics. Hash stable canonical booking fields rather than raw JSON serialization.

## Retry semantics

For the same idempotency key:
- IN_PROGRESS → return current state,
- CONFIRMED → return the same result,
- FAILED → a new operation version may be allowed by policy,
- UNKNOWN → do not issue a new provider create; reconcile.

## Failure modes

Short TTLs, generating new keys for the same intent, multi-region races, lock-only protection, unstable request hashes and lost provider-key mappings can all bypass duplicate prevention.

## Observability

Track idempotency hit rate, blocked duplicate attempts, key conflicts, blocked UNKNOWN retries, lock contention and duplicate provider-reference detection.

## Production checklist

Use operation-scoped deterministic keys, persistent uniqueness, request hashes, provider-key mapping, UNKNOWN retry blocking, explicit TTL policy, audit logs and multi-node concurrency tests.
