$ open blueprint
A ticket sale isn't a normal write operation. For one brief window the moment a popular event goes on sale thousands of people can try to buy the same seat at the same second.
Most systems handle this badly. They sell 500 tickets for a 400-capacity venue, then spend the next three days apologizing over email. The failure isn't in the UI or the payment provider. It's in the architecture's assumption that "check inventory, then reserve it" is a single, safe operation. It isn't not under concurrency.
This platform exists to make that failure structurally impossible, while giving organizers a system that runs an event end to end: creation, sales, payment, and the door.
Executive Summary
Ticketing looks like a CRUD problem until the first high-demand event goes live. Then it becomes a concurrency problem, a payments problem, and a fraud-prevention problem simultaneously.
| Business Impact | Engineering Impact |
|---|---|
| Zero overselling, even at peak demand | Row-level locking on inventory during booking |
| Trustworthy payment reconciliation | Idempotent, webhook-driven payment confirmation |
| Faster event setup and go-live | Explicit lifecycle state machine, no ad-hoc statuses |
| Reliable, fast entry-gate check-in | QR validation that works offline-tolerant at the door |
| Confidence to run flash sales and drops | Reservation holds that expire deterministically |
For founders, this is the difference between an event page that can survive a ticket drop going viral and one that falls over or worse, sells tickets that don't exist.
For engineers, this is a textbook case of where "it works in testing" and "it works under real concurrent load" are two completely different claims, and only one of them matters on launch day.
The Business Problem
Picture an organizer launching ticket sales for a 2,000-capacity concert, with 8,000 people refreshing the page at 10:00 AM sharp.
Without the right architecture:
- Two requests both read "3 tickets left," both proceed, and 4 tickets get sold for 3 seats
- A payment fails after the ticket was already marked as sold, and now a phantom ticket exists with no paid order behind it
- A user double-clicks "buy" and gets charged twice, or reserves the same seat twice
- At the door, staff have no fast, reliable way to confirm a QR code hasn't already been scanned five minutes ago at another gate
Every one of these is a real, common failure mode in ticketing systems that treat booking as "just another database write."
Why Founders Should Care
The Cost of Getting Concurrency Wrong
- Overselling an event means refunds, reputational damage, and sometimes legal exposure
- Payment/inventory mismatches mean manual reconciliation after every event
- A booking flow that buckles under a traffic spike is a launch-day failure, not a bug to fix next sprint
- Fraudulent duplicate check-ins (screenshotted or shared QR codes) directly cost revenue
The Leverage of Getting It Right
- Organizers can run genuine flash sales and time-limited drops without fear
- Payment reconciliation becomes automatic instead of a manual spreadsheet exercise after every event
- Entry operations move fast because check-in validation is a single, unambiguous lookup, not a judgment call by door staff
- The platform scales to festivals and multi-day conferences without a rewrite, because the core booking guarantees don't change with volume
Why Engineers Should Care
This system sits at the intersection of three hard problems that don't forgive shortcuts:
- Concurrency many actors racing for the same finite resource (a seat, a ticket tier)
- Distributed consistency a payment provider confirms a charge asynchronously, on its own schedule, not the request-response cycle's
- Idempotency the same webhook, the same retry, the same double-click must never produce two tickets or two charges
Get any one of these wrong and the failure isn't cosmetic it's tickets that don't exist, money that vanished, or a door that lets someone in twice on one QR code.
High-Level Architecture Blueprint
┌───────────────────────────┐
│ Client Applications │
│ (Web, Mobile, Box Office) │
└───────────┬───────────────┘
│
▼
┌───────────────────────┐
│ API Gateway │
└───────────┬───────────┘
│
┌───────────────┬─────────────┼─────────────────┬──────────────────┐
▼ ▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Event │ │ Booking & │ │ Payment │ │ Check-In │ │ Identity │
│ Lifecycle │ │ Inventory │ │ Service │ │ & Access │ │ & RBAC │
│ Domain │ │ Engine │ │ │ │ Control │ │ │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │ │ │
└────────────────┴─────────┬──────┴───────────────┴───────────────┘
▼
┌──────────────────────────┐
│ Shared Service Layer │
│ (Workflow Orchestration) │
└─────────────┬────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌───────────────┐
│ PostgreSQL │ │ Redis Locks │ │ Webhook Queue │
│ (Bookings, │ │ (Reservation│ │ (Payment │
│ Tickets) │ │ Holds) │ │ Events) │
└─────────────┘ └─────────────┘ └───────────────┘
Core Responsibilities
| Component | Responsibility |
|---|---|
| Event Lifecycle Domain | Event creation, publishing, capacity, and status transitions |
| Booking & Inventory Engine | Concurrency-safe seat/ticket reservation and release |
| Payment Service | Charge initiation, webhook verification, idempotent confirmation |
| Check-In & Access Control | QR issuance, validation, and entry audit records |
| Identity & RBAC | Admin, Organizer, and Attendee role enforcement |
| Shared Service Layer | Orchestrates booking → payment → ticket issuance as one flow |
| PostgreSQL | System of record for events, bookings, and tickets |
| Redis | Short-lived reservation locks and hold expiry |
Event Lifecycle State Machine
An event is never just "active" or "not active." It moves through explicit, enforced states and each transition has rules about what's allowed.
Draft
│
▼
Published
│
▼
On Sale
│
▼
Sold Out ──────┐
│ │
▼ ▼
In Progress ← Sales Closed
│
▼
Completed
│
▼
Archived
Why an Explicit State Machine, Not Status Flags
Bad
if event.is_active and not event.is_cancelled and event.tickets_left > 0:
# scattered conditionals reconstructing "can this be booked"
Good
if event.status.allows_booking():
# the state itself knows what it permits
Every status flag you add multiplies the number of combinations someone has to reason about. A state machine makes illegal states like "sold out but still accepting bookings" impossible to represent in the first place.
Booking-First Reservation Workflow
The core insight: never let two requests believe they both got the last ticket. Reserve first, confirm second.
Ticket Request Received
│
▼
Acquire Row-Level Lock on Ticket Tier
│
▼
Check Available Inventory
│
▼
Create Reservation Hold (expires in N minutes)
│
▼
Release Lock
│
▼
Redirect to Payment
│
▼
Payment Confirmed?
├─ Yes → Convert Hold to Confirmed Ticket
└─ No → Release Hold, Return Inventory
Concurrency Safety in Practice
Bad
if event.tickets_available > 0:
event.tickets_available -= 1
create_ticket()
# Two concurrent requests can both pass the check
# before either decrements classic race condition.
Good
with transaction.atomic():
tier = TicketTier.objects.select_for_update().get(id=tier_id)
if tier.tickets_available > 0:
tier.tickets_available -= 1
tier.save()
create_reservation_hold()
select_for_update() forces concurrent requests to queue for the same row instead of racing against a stale read. This single line is the difference between "never oversold" and "oversold under load, worked fine in every manual test."
Reservation Holds, Not Instant Commits
A ticket isn't sold the moment someone clicks "buy" it's held. Holds expire automatically if payment isn't completed in time, releasing inventory back to the pool without anyone needing to intervene.
Hold Created: 10:03:00
Expires: 10:08:00
Payment received at 10:07:40 → Hold converts to Ticket
Payment not received by 10:08:00 → Hold released, inventory returned
Payment Processing: Idempotency by Design
Payment providers confirm charges via webhook, on their schedule, not the user's request cycle and webhooks can and do arrive more than once.
The Problem
Webhook fires for payment_intent_8842
Network hiccup → provider retries
Webhook fires again for payment_intent_8842
Bad
def handle_webhook(event):
create_ticket(event.booking_id)
# Called twice → two tickets issued for one payment
Good
def handle_webhook(event):
if PaymentEvent.objects.filter(
provider_event_id=event.id
).exists():
return # already processed, safely ignored
with transaction.atomic():
PaymentEvent.objects.create(provider_event_id=event.id)
confirm_booking(event.booking_id)
Why This Matters
Idempotency keys turn "the network is unreliable" from a data-integrity risk into a non-issue. The webhook can fire once, three times, or a hundred times the outcome is identical every time.
QR-Based Access Control
Ticket Issuance
Payment Confirmed
│
▼
Generate Unique Ticket Token (signed, non-guessable)
│
▼
Encode as QR Code
│
▼
Deliver to Attendee
Validation at the Door
QR Scanned at Gate
│
▼
Verify Signature (not tampered / forged)
│
▼
Check Ticket Status
├─ Valid, Unused → Mark as Checked In, Admit
├─ Already Used → Reject, Flag for Staff Review
└─ Invalid/Unknown → Reject
│
▼
Write Entry Audit Record
Why Signed Tokens, Not Just Ticket IDs
Bad
QR encodes: ticket_id=10293
Trivially guessable and shareable anyone can generate a fake QR with a sequential ID.
Good
QR encodes: signed_token (ticket_id + expiry + HMAC signature)
A signed token can be verified offline at the gate without a live database round trip for the signature check itself, and it can't be forged without the signing key.
Preventing Duplicate Entry
The "already used" check must be atomic across every gate at the venue, not just the one that scanned it. Two gates scanning the same shared screenshot within the same second must not both admit the check-in write is the same row-level locking pattern as ticket booking, applied to entry instead of inventory.
Multi-Tenant & Role Model
Organization (Organizer Account)
│
├── Events (tenant-scoped)
├── Ticket Tiers (tenant-scoped)
├── Staff / Door Roles (tenant-scoped)
└── Financial Records (tenant-scoped)
Role Model
| Role | Can Do |
|---|---|
| Admin | Full platform control, cross-organization oversight |
| Organizer | Create/manage events, view sales, manage staff |
| Door Staff | Scan and validate tickets only no sales or event data access |
| Attendee | Purchase tickets, view own bookings and QR codes |
Door staff getting scoped, minimal access matters in practice a lost or compromised staff device should never expose sales data or the ability to issue tickets, only the ability to scan them.
Common Mistakes in Ticketing Systems
Checking Inventory Without Locking It
Bad
if tickets_available > 0:
sell_ticket()
Good
with transaction.atomic():
tier = TicketTier.objects.select_for_update().get(id=tier_id)
# decrement inside the lock, not after a separate read
Treating Webhooks as Guaranteed Single-Delivery
Bad
Assume each webhook call happens exactly once
Good
Deduplicate by provider event ID before processing
Issuing Tickets Before Payment Is Confirmed
Bad
Ticket created immediately on checkout click,
payment processed afterward
Good
Reservation hold on checkout click,
ticket created only after payment confirmation
An unpaid "ticket" that exists in the system is inventory the platform can never sell to someone who would actually pay for it.
Evolution Path
Basic Event + Ticket CRUD
│
▼
Booking-First Reservation Model
│
▼
Concurrency-Safe Inventory Locking
│
▼
Webhook-Driven Payment Confirmation
│
▼
QR-Based Access Control
│
▼
Multi-Gate, High-Volume Check-In
│
▼
Real-Time Sales Analytics & Fraud Detection
The booking-first model and row-level locking aren't optional additions for later they need to exist before the first high-demand event, because that's precisely when a naive implementation fails.
Real-World Examples
Eventbrite
Known for making self-serve event creation and ticketing accessible to organizers of any size, with reservation flows built to hold under real demand spikes.
Ticketmaster
Known for operating ticketing at a scale where queueing and inventory-locking aren't optional they're the entire product, given how frequently high-demand drops sell out in seconds.
Cvent
Known for extending ticketing into full event operations registration, check-in, and post-event reporting for large conferences and corporate events.
These platforms share a common thread: the sales page is the easy part. The reservation engine underneath it is what actually gets tested under real load.
What I Would Build Today
If I were building an event ticketing platform today, my starting stack would be:
- Django + Django REST Framework
- PostgreSQL with
select_for_update()for every inventory-affecting write - Redis for short-lived reservation holds and lock coordination
- Webhook-driven payment confirmation with an idempotency table from day one
- Signed, offline-verifiable QR tokens for entry
- An explicit event lifecycle state machine instead of boolean status flags
What I Would Avoid Initially
- Optimistic "check then write" inventory logic it works in every test and fails on the one day it matters
- Trusting a payment provider's webhook to arrive exactly once
- Building a custom queueing/waiting-room system before proving the booking engine itself holds up under load
The features that make a ticketing platform impressive flash sales, instant check-in, live capacity dashboards are only trustworthy if the booking engine underneath them cannot oversell. That has to be correct before anything else is built on top of it.
Key Takeaways
Ticketing is a concurrency problem wearing a checkout-flow costume.
A well-designed event ticketing platform provides:
- Inventory guarantees that hold under real concurrent demand, not just in single-user testing
- Payment confirmation that's idempotent by construction, immune to retries and duplicate webhooks
- An explicit event lifecycle that makes invalid states impossible to represent
- Entry validation that's fast, offline-tolerant, and impossible to duplicate across gates
- A foundation organizers can trust with a flash sale on day one, not just a slow, steady ticket trickle


