Skip to content

sriddbs/eventify

Repository files navigation

ToucanTix Senior Rails Developer Technical Assessment

Venue Event Capacity Manager - Starter Repository

Welcome! This is the starter repository for the ToucanTix technical assessment. Your challenge is to build a capacity-controlled event management system with a GraphQL API.

Challenge Overview

Time Expectation: ~3 hours

The time allocations in each section are suggestions to help you prioritize. We care most about your decisions, communication, and approach — complete what you can and document your thinking. Quality and thoughtfulness are more important than finishing everything.

Scenario: ToucanTix needs a new feature for venues to manage capacity-controlled events with different ticket types. Serengeti Park wants to run a special 'Evening Safari' with limited slots.

What's Provided

This starter repository includes:

  • Rails API application configured with PostgreSQL
  • GraphQL gem installed and basic setup complete
  • Venue model (already created and seeded)
  • RSpec, FactoryBot, and testing gems configured
  • Basic project structure

What You Need to Implement

Core Models to Create

  • Event (belongs to Venue)
  • TicketType (belongs to Event)
  • Booking (tracks purchases)

Must Support

  1. Create an event with multiple ticket types (Adult, Child, Member)
  2. Each ticket type has its own capacity and pricing
  3. Atomic booking creation that prevents overbooking
  4. GraphQL endpoint to check real-time availability
  5. Handle concurrent bookings safely

Part 1: Database & Models (~45 min suggested)

  • PostgreSQL schema design
  • Rails models with validations
  • One migration file

Part 2: Core Business Logic (~90 min suggested)

Implement a CreateBookingService that:

  • Validates ticket availability
  • Handles atomic operations to prevent race conditions
  • Returns clear errors when capacity exceeded
  • Uses appropriate locking strategy (pessimistic or optimistic)

Critical Requirement: Your solution MUST handle concurrent bookings safely. When 100 people try to book the last ticket simultaneously, only one should succeed.

Part 3: GraphQL API (~45 min suggested)

Implement the following GraphQL schema:

# Query for checking availability
query EventAvailability($eventId: ID!, $date: ISO8601Date!) {
  event(id: $eventId) {
    name
    ticketTypes {
      name
      price
      remainingCapacity(date: $date)
    }
  }
}

# Mutation for creating bookings
mutation CreateBooking($input: BookingInput!) {
  createBooking(input: $input) {
    booking {
      id
      confirmationCode
    }
    errors
  }
}

# BookingInput should include:
# - eventId: ID!
# - date: ISO8601Date!
# - ticketSelections: [TicketSelectionInput!]!
#
# TicketSelectionInput should include:
# - ticketTypeId: ID!
# - quantity: Int!

Part 4: Testing & Documentation (~30 min suggested)

  • Write tests demonstrating your booking logic works correctly
  • Consider concurrent scenarios (note: realistic race condition testing is challenging — document your approach rather than building complex test infrastructure)
  • Complete the "Your Solution" section below
  • See spec/requests/venues_spec.rb for a working request spec template

Your Solution

Design Decisions

1. Why this approach for handling capacity

  • Availability is derived dynamically from BookingTicket instead of a persisted counter.
  • This ensures capacity is always consistent with actual bookings and cancellations.
  • The SELECT ... FOR UPDATE lock on TicketType provides a safe serialization point.

2. How race conditions are prevented

  • All relevant TicketType rows are locked before reading capacity.
  • Locks are held during availability computation and booking creation.
  • Concurrent transactions wait for the first to commit or roll back.
  • This ensures atomicity, if 100 users try to book the last ticket, only one succeeds.

3. Booking Status

  • Currently, only "booked" status exists. Availability queries filter by this status.
  • In real-world systems, a booking typically has multiple stages: "pending", "booked", "cancelled", "failed".
  • Only "booked" bookings decrement capacity. Pending or failed bookings do not.
  • Future improvements: integrate payment gateways, use a state machine, and consider timeouts for pending bookings to release seats.

4. Multi-Tenant Isolation

  • venue_id is added to all relevant tables (bookings, booking_tickets, ticket_types) to support multi-tenant data separation. Queries can be filtered by venue_id for safety.

5. Trade-offs made

  • Strong consistency, no stale counters, simple reasoning.
  • Slightly reduced concurrency due to row locks.
  • Aggregation via SUM(:quantity) adds minimal cost but avoids complexity.

What I'd Add With More Time

  • Ticket inventory or counter-based model: Instead of computing sold quantities with an aggregate query, I'd maintain a sold counter or a separate ticket_inventories table (scoped by event_id and date). This would make availability queries faster and simplify read-heavy endpoints like "Show remaining tickets."

  • Support for recurring events: Currently, the design assumes one event per date. For multi-day or recurring events, we'd need a proper event_occurrences table and availability tracking per (event, date, ticket_type).

  • Booking cancellation adjustments: On cancellation, we'd need to safely decrement the sold counter or recalculate availability to prevent undercounting or double releases.

  • Index and query optimizations: Probably a covering index that INCLUDEs quantity could improve the performance of the availability aggregation query, as it avoids fetching full rows during SUM(:quantity) operations.

  • Caching strategies:

    • Implement read-through caching for high-read queries such as availability checks or event listings

    • Cache computed availability counts (e.g., {event_id, ticket_type_id} -> remaining_tickets)

    • Use Redis or Memcached for short-term TTL caching to reduce DB load under high traffic.

    • Invalidate or update cache post-transaction commit.

  • Better error granularity: Differentiate between "event mismatch," "ticket sold out," and "invalid quantity" errors for clearer client responses.

  • Concurrency Testing Approach: Realistic race conditions are hard to simulate in tests. We can use a script with multiple processes (Process.fork) to emulate simultaneous bookings. Stress test with random quantities and multiple "users" to verify the service never oversells. Print summary results to ensure capacity is never exceeded.

Questions I'd Ask the Product Team

  • ...

Evaluation Criteria

We're looking for:

  • ✅ Database design that handles concurrent access
  • ✅ Understanding of Rails transactions and locking
  • ✅ Clean service object pattern
  • ✅ GraphQL resolver efficiency (N+1 prevention)
  • ✅ Test coverage on the critical path

Bonus points for:

  • Considering multi-tenant isolation
  • Caching strategies
  • Edge cases (refunds, partial bookings)
  • Clear documentation of decisions

Tips

  1. Start with the models - Get the associations right first
  2. Focus on the race condition - This is the most critical part
  3. Write at least one integration test - Show the full booking flow works
  4. Document your approach - We value clear thinking over perfect code
  5. Use Rails conventions - We're looking for idiomatic Rails code

Submission

When complete:

  1. Ensure all tests pass
  2. Fill out the "Your Solution" section above
  3. Commit your changes with clear, descriptive commit messages
  4. Submit your work using the following method:
git bundle create toucan-challenge.bundle --all

Email us the .bundle file. We can clone it like a normal repo.


Feel free to use any Rails patterns you prefer (concerns, services, form objects, etc). We care more about clarity and correctness than following a specific pattern.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages