Core Entities
Venue: venue_id, name, address, capacity, type (ARENA, CONFERENCE_CENTER, THEATER, OUTDOOR), facilities (JSON: {parking, catering, av_equipment}), timezone, is_active. Event: event_id, organizer_id, venue_id, title, description, category (CONCERT, CONFERENCE, SPORTS, FESTIVAL), status (DRAFT, PUBLISHED, ON_SALE, SOLD_OUT, CANCELLED, COMPLETED), start_time, end_time, cover_image_url, age_restriction. TicketTier: tier_id, event_id, name (GENERAL, VIP, EARLY_BIRD, BACKSTAGE), price, total_quantity, sold_quantity, available_quantity, sale_start, sale_end, max_per_order. Order: order_id, event_id, buyer_id, status (PENDING, CONFIRMED, CANCELLED, REFUNDED), total_amount, payment_id, created_at, expires_at. OrderItem: item_id, order_id, tier_id, quantity, unit_price, subtotal. Ticket: ticket_id, order_id, tier_id, attendee_name, attendee_email, qr_code (unique token), status (VALID, USED, CANCELLED, TRANSFERRED), issued_at. VenueBooking: booking_id, venue_id, event_id, start_time, end_time, setup_hours_before, teardown_hours_after, status, deposit_paid.
Ticket Purchase with Inventory Control
class TicketingService:
ORDER_TTL_MINUTES = 15
def reserve_tickets(self, event_id: int, buyer_id: int,
selections: list[dict]) -> Order:
with self.db.transaction():
order = Order(
event_id=event_id, buyer_id=buyer_id,
status=OrderStatus.PENDING,
expires_at=datetime.utcnow() + timedelta(minutes=self.ORDER_TTL_MINUTES)
)
items = []
for sel in selections:
tier = self.db.query(
'SELECT * FROM ticket_tiers WHERE tier_id=:t FOR UPDATE',
t=sel['tier_id']
)
if tier.available_quantity tier.max_per_order:
raise MaxPerOrderError(tier.max_per_order)
if not (tier.sale_start <= datetime.utcnow() <= tier.sale_end):
raise SaleNotActiveError()
tier.available_quantity -= sel['quantity']
tier.sold_quantity += sel['quantity']
self.db.save(tier)
items.append(OrderItem(
tier_id=tier.tier_id,
quantity=sel['quantity'],
unit_price=tier.price,
subtotal=tier.price * sel['quantity']
))
order.total_amount = sum(i.subtotal for i in items)
self.db.save(order)
for item in items:
item.order_id = order.order_id
self.db.save(item)
# Set expiry timer in Redis
self.redis.setex(f'order_expiry:{order.order_id}',
self.ORDER_TTL_MINUTES * 60, '1')
return order
def release_expired_orders(self):
# Scheduled job every minute — release unpaid expired reservations
expired = self.db.query(
'SELECT * FROM orders WHERE status='PENDING' AND expires_at < NOW()'
)
for order in expired:
for item in order.items:
self.db.execute(
'UPDATE ticket_tiers SET available_quantity=available_quantity+:q, ' +
'sold_quantity=sold_quantity-:q WHERE tier_id=:t',
q=item.quantity, t=item.tier_id
)
order.status = OrderStatus.CANCELLED
self.db.save(order)
Venue Booking and Conflict Detection
A venue cannot host two events that overlap in time (including setup and teardown). Booking conflict check: SELECT COUNT(*) FROM venue_bookings WHERE venue_id = :v AND status NOT IN (‘CANCELLED’) AND start_time :new_start. Where new_start = event.start_time – setup_hours and new_end = event.end_time + teardown_hours. If count > 0: conflict exists. Transaction lock: SELECT … FOR UPDATE on venue_bookings for the venue to prevent concurrent bookings from both passing the check. Booking deposit: venues typically require a deposit to confirm a booking. Store deposit_paid amount and payment_id. On deposit payment: status transitions from REQUESTED to CONFIRMED. Catering and AV add-ons: stored as VenueBookingAddOn (addon_id, booking_id, type, description, cost). Included in the venue invoice.
Check-In and Ticket Validation
QR code: each ticket has a unique UUID token stored as a QR code. At the venue: scanner app reads QR code, sends token to the validation API. Validation API: SELECT * FROM tickets WHERE qr_code = :token. Check status = VALID. If valid: update status = USED, record check_in_time. Return ADMIT. If status = USED: return ALREADY_ADMITTED (duplicate scan). If status = CANCELLED: return INVALID. Response time: < 200ms including database round trip. Caching: cache valid ticket tokens in Redis (TTL = event duration). Most validations hit Redis without a DB query. On ticket use: invalidate the Redis cache key and update the DB asynchronously. Offline mode: the scanner app downloads all valid tokens for the event before doors open. Validates locally if network is unavailable. Syncs used tickets when connectivity returns. Transfer: a ticket holder can transfer to another person. CREATE a new Ticket record for the transferee, mark the original as CANCELLED, email the QR code. Limit transfers to once per ticket to prevent scalping chains.
Event Discovery and Search
Search: full-text Elasticsearch index on event title, description, and venue name. Filter by: category, date range, location (geosearch by venue coordinates), price range, availability (has_available_tickets). Sort: relevance (default), date, price. Geosearch: Elasticsearch geo_distance query on venue location. Find events within X km of the user’s location. Personalization: if user is logged in, boost events in categories they have attended before. Trending: events with the most ticket sales in the last 24 hours — maintained as a Redis sorted set, updated on each order confirmation. Homepage: top 10 from the trending set, filtered to events in the user’s city. Waitlist: when an event sells out, users can join a waitlist (tier-specific). On cancellation, notify the next waitlisted user with a 30-minute window to purchase the released ticket.
See also: Airbnb Interview Prep
See also: Stripe Interview Prep
See also: Shopify Interview Prep