QR Ticket Validation System Design: From Offline to Idempotency
A ticket inspector scans a QR code, the system checks it in the database and marks it as used. The core requirement: one ticket, one entry. A seemingly simple task becomes complicated by network failures, queues, and multiple inspectors. Let's explore the evolution of solutions from offline validation to reliable server-side processing.
Offline mode uses a digital signature (ECC or RSA). The QR code contains the ticket data and signature; the inspector verifies it with a public key without needing a network connection. The problem: lack of synchronization between inspectors. A screenshot of the ticket can be used at each gate independently.
Server-Side State and Atomic Updates
A centralized storage system solves the synchronization problem. Each scan is a request to the server with an atomic status update.
Example in PostgreSQL:
UPDATE tickets
SET status = 'used', used_at = NOW()
WHERE id = :ticket_id AND status = 'active'
RETURNING *;
If a row is returned, the ticket is redeemed and entry is granted. If no row is returned, the ticket is already used or does not exist.
A classic problem: losing the server response. An inspector in a low-signal area sends a request, the server updates the database, but the response doesn't arrive. A timeout occurs, a rescan is attempted—the server denies it, and the client cannot enter despite having a paid ticket.
Idempotency at the API Level
The solution is idempotent requests with a unique key (UUID), generated when opening a scanning session. The key records the attempt, allowing retries without duplication.
Logic within a transaction (read committed):
begin ;
-- 1. Record the attempt
INSERT INTO processed_requests (ticket_id, idempotency_key)
VALUES (:ticket_id, :idempotency_key)
ON CONFLICT (ticket_id, idempotency_key)
DO NOTHING RETURNING *;
-- 2. If rows_affected == 0: rollback, return SUCCESS
-- 3. Redeem the ticket
UPDATE tickets
SET status = 'used', used_at = NOW()
WHERE id = :ticket_id AND status = 'active'
RETURNING *;
-- 4. If rows_affected == 0: rollback, return ERROR
commit;
-- 5. Return SUCCESS
The transaction ensures atomicity: either the key and status are updated, or nothing is. A unique index on (ticket_id, idempotency_key) prevents race conditions—the second request waits or goes to DO NOTHING.
A retry within the same session returns success, even if the first response was lost.
Head-of-Line Blocking and Context Loss
A real problem: an inspector under pressure from a queue closes the session without a response. A new scan generates a fresh idempotency_key. The server sees a used ticket with a different key—denial.
The inspector cannot distinguish a legitimate client from a fraudster with a screenshot. Head-of-line blocking requires a final response before the next ticket.
A compromise: return the timestamp of the last use. The inspector assesses: "2 minutes ago—likely the same person." Risk: a group with a shared QR code passes sequentially.
Key challenges:
- Network timeouts without state loss
- Synchronization of multiple inspectors
- Human factors in queues
- Balancing reliability and UX
Key Takeaways
- Atomic UPDATE with a condition status='active' prevents double redemption
- Idempotency via UUID + unique index guarantees retries without duplicates
- Transactions (read committed) ensure sequential visibility of changes
- Head-of-line blocking is inevitable for 100% reliability without biometrics
- The timestamp compromise shifts the decision to the operator
Future Improvements
Static QR codes are becoming outdated. Dynamic tokens or challenge-response with one-time OTPs solve the problem. The inspector requests a nonce from the server, the client signs it—the server verifies and marks it atomically.
Biometrics (face ID) or NFC eliminate screenshots but require hardware. QUIC minimizes packet loss but doesn't solve context loss.
Scaling: Redis for caching idempotency_keys, sharding by ticket_id. Monitoring: metrics for failed scans by timestamp, A/B testing of compromises.
— Editorial Team
No comments yet.