Low-Level Design: Event Management System — Venue Booking, Ticketing, and Attendee Management

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.

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

Scroll to Top