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.
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.
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
- Event (belongs to Venue)
- TicketType (belongs to Event)
- Booking (tracks purchases)
- Create an event with multiple ticket types (Adult, Child, Member)
- Each ticket type has its own capacity and pricing
- Atomic booking creation that prevents overbooking
- GraphQL endpoint to check real-time availability
- Handle concurrent bookings safely
- PostgreSQL schema design
- Rails models with validations
- One migration file
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.
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!- 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.rbfor a working request spec template
1. Why this approach for handling capacity
- Availability is derived dynamically from
BookingTicketinstead of a persisted counter. - This ensures capacity is always consistent with actual bookings and cancellations.
- The
SELECT ... FOR UPDATElock onTicketTypeprovides a safe serialization point.
2. How race conditions are prevented
- All relevant
TicketTyperows 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_idis added to all relevant tables (bookings, booking_tickets, ticket_types) to support multi-tenant data separation. Queries can be filtered byvenue_idfor 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.
-
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.
- ...
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
- Start with the models - Get the associations right first
- Focus on the race condition - This is the most critical part
- Write at least one integration test - Show the full booking flow works
- Document your approach - We value clear thinking over perfect code
- Use Rails conventions - We're looking for idiomatic Rails code
When complete:
- Ensure all tests pass
- Fill out the "Your Solution" section above
- Commit your changes with clear, descriptive commit messages
- Submit your work using the following method:
git bundle create toucan-challenge.bundle --allEmail 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.