List the key functional requirements for the system (Ask the AI for hints if stuck)...
Browse and search available rooms: Users search for rooms by time slot, capacity, building, floor, and equipment (projector, video conferencing). The system returns only rooms that are actually free during the requested window. This is the most frequent operation and must feel instant.
Create, update, and cancel bookings: A user selects a room and time slot, and the system reserves it. The booking includes the room, time range, organizer, and optionally a list of participants. Users can modify the time or room, or cancel entirely. Each mutation must re-validate availability to prevent conflicts.
No double-booking: This is the single hardest requirement. When two people simultaneously try to book the same room for overlapping times, exactly one must succeed and the other must fail immediately. There is no "eventually consistent" middle ground here; a room is either booked or it is not.
Notifications: The system sends confirmation emails when a booking is created, updated, or cancelled, plus reminders before meetings start. These are asynchronous and must not block the booking flow.
List the key non-functional requirements (performance, scalability, reliability, etc.)...
strong consisitency, high avaliblility, high scalability
low latency for searches
Strong consistency for booking writes: Booking operations must be ACID-compliant. When two concurrent requests target the same room and time, the database must serialize them so only one succeeds. This is non-negotiable.
High availability for reads: Room search and availability queries are read-heavy and should maintain 99.9%+ uptime. Read replicas or caching can serve availability data even during partial outages.
Low latency for search: Browsing available rooms should respond in under 200ms. Booking operations can tolerate slightly higher latency (under 1 second) because users expect a brief pause when confirming a reservation.
Estimate the scale of the system. Consider daily active users, read/write ratio, storage requirements, bandwidth, and any relevant QPS calculations...
Conference room booking is a low-to-moderate scale system compared to consumer-facing platforms. The numbers matter not because scale is the challenge, but because they reveal that the system is write-light and read-heavy, which directly shapes the architecture.
Organization scale: A large enterprise with 100,000 rooms across multiple buildings and 1 million daily active users. This is the upper bound; most deployments are far smaller.
Booking writes: 2 million bookings per day. Spread across an 8-hour business window (most bookings happen during work hours): 2M / (8 x 3,600) = roughly 70 bookings per second at peak. This is very manageable for a single database instance with proper indexing.
Availability reads: Users browse rooms far more often than they book. At a 10:1 read-to-write ratio: 700 availability queries per second at peak. Still modest, but caching becomes worthwhile to keep response times under 200ms.
Read-to-write ratio: roughly 10:1: This tells us the architecture should optimize for fast reads (caching, read replicas) while ensuring writes are strongly consistent. A single PostgreSQL instance handles the write load comfortably; the read path benefits from a Redis cache.
Interview Tip
These numbers reveal something important: the booking system is not a scale problem. A single well-provisioned PostgreSQL instance handles 70 writes/sec easily. The real challenge is correctness (preventing double-bookings) not throughput. This is the opposite of systems like view counters or social feeds where scale drives the architecture.
Room metadata: 100,000 rooms x 1KB per room (name, building, floor, capacity, equipment list) = 100MB. This fits entirely in memory, making room search extremely fast when cached.
Booking records: Each booking is roughly 200 bytes (room_id, user_id, start_time, end_time, status, created_at). At 2 million bookings per day: 400MB per day, about 150GB per year. After a few years, old bookings can be archived to cold storage since only future and recent past bookings are actively queried.
Redis cache: Room availability data for 100K rooms over 30 days of booking windows. Each room-day slot requires roughly 100 bytes. 100K rooms x 30 days x 100 bytes = 300MB. This fits comfortably in a single Redis instance.
Define the APIs expected from the system. This is your chance to analyze and define the read and write paths so that you can come up with the high-level design...
GET /api/v1/rooms/available
Query params:
start_time: ISO-8601
end_time: ISO-8601
capacity_min: number (optional)
building: string (optional)
floor: number (optional)
equipment: string[] (optional, e.g., "projector,video")
Response: 200 OK
{
rooms: [
{
room_id: string,
name: string,
building: string,
floor: number,
capacity: number,
equipment: ["projector", "whiteboard"]
}
]
}
The start_time and end_time parameters are required because availability is meaningless without a time window. The response includes only rooms that are completely free during the entire requested period, not rooms with partial availability.
POST /api/v1/bookings
Headers: X-Idempotency-Key: string
Body:
{
room_id: string,
start_time: ISO-8601,
end_time: ISO-8601,
title: string,
participants: string[] (optional, user IDs)
}
Response: 201 Created
{
booking_id: string,
room_id: string,
start_time: ISO-8601,
end_time: ISO-8601,
status: "confirmed"
}
Error: 409 Conflict (room already booked for overlapping time)
The 409 Conflict response is critical: it explicitly tells the client that the booking failed due to a scheduling conflict, not a server error. The client can then prompt the user to choose a different room or time without guessing why the request failed.
The X-Idempotency-Key header prevents duplicate bookings from network retries. If a client times out and retries, the server recognizes the duplicate key and returns the existing booking instead of creating a second one.
PUT /api/v1/bookings/:booking_id
Body: { start_time, end_time, room_id (optional) }
Response: 200 OK { updated booking }
Error: 409 Conflict (new time/room conflicts)
DELETE /api/v1/bookings/:booking_id
Response: 204 No Content
Modification re-validates availability for the new time or room. It is effectively a cancel-and-rebook in a single transaction, ensuring no window exists where the old slot is freed but the new slot is not yet reserved.
POST /api/v1/rooms (add room)
PUT /api/v1/rooms/:room_id (update attributes)
DELETE /api/v1/rooms/:room_id (decommission)
Admin endpoints are low-frequency and require elevated permissions. Decommissioning a room must handle existing future bookings by notifying affected users and cancelling their reservations.
Describe the overall system architecture. Identify the main components needed to solve the problem end-to-end. Use the diagramming tool to create a block diagram.
Client to API Gateway: The user selects a room and time slot and submits a booking request. The API gateway authenticates the user, rate-limits requests, and routes to the Booking Service.
Booking Service: The core component. It receives booking requests, checks availability within a database transaction (SELECT FOR UPDATE), creates the booking record, invalidates the relevant cache entries, and publishes a notification event to the message queue. All of this happens in a single request-response cycle, keeping the user waiting under 1 second.
PostgreSQL: The source of truth for all room and booking data. Handles concurrent booking requests through row-level locking, ensuring only one booking succeeds for any given room and time slot.
Message Queue (RabbitMQ or SQS): Decouples booking confirmation from notification delivery. After a booking commits, an event is published for the Notification Service to process asynchronously.
Availability Service: Handles room search and availability queries. Checks Redis cache first; on cache miss, queries PostgreSQL and populates the cache. Stateless and horizontally scalable.
Redis Cache: Stores pre-computed availability data per room per day. Serves the majority of read requests without touching the database. Cache entries are invalidated when bookings change.
Notification Service: Consumes booking events from the message queue and sends confirmation emails, update notifications, and meeting reminders. Failures are retried with exponential backoff. A dead-letter queue captures permanently failed notifications.
If Redis goes down, the Availability Service falls back to direct PostgreSQL queries. Response times increase from under 5ms to roughly 50-100ms, but the system remains fully functional. Booking operations are completely unaffected because they always go through the database directly.
Step 1: Client submits booking request: The user selects a room and time from the search results and clicks "Book." The client sends POST /api/v1/bookings with room_id, start_time, end_time, and an idempotency key.
Step 2: API Gateway authenticates and rate-limits: Verifies the user's session token and checks rate limits (prevent a single user from spamming booking requests). Routes to the Booking Service.
Step 3: Booking Service checks idempotency key: Looks up the key in the database. If it exists, returns the existing booking immediately (this is a retry).
Step 4: Database transaction begins: The service starts a PostgreSQL transaction and runs SELECT FOR UPDATE to check for overlapping bookings on the target room and time.
Step 5: Availability check: If overlapping confirmed bookings exist, the transaction rolls back and the service returns 409 Conflict. If no overlap, the service proceeds.
Step 6: Booking record inserted: The new booking row is inserted with status "confirmed" along with the idempotency key. The transaction commits.
Step 7: Cache invalidation: The service deletes the Redis cache entry for the affected room and date. This happens outside the transaction (best-effort). If it fails, the cache will expire via TTL.
Step 8: Notification published: A booking event is published to the message queue for the Notification Service. If publishing fails, the event is retried with a local outbox table.
Step 9: Response returned: 201 Created with the booking details, including booking_id and confirmation status.
Step 1: User sends DELETE /api/v1/bookings/:booking_id. Step 2: Booking Service verifies the user owns the booking. Step 3: Updates booking status to "cancelled" (soft delete for audit history). Step 4: Invalidates the cache for the affected room and date. Step 5: Publishes a cancellation event for participant notification.
Step 1: Client sends search request: The user specifies a time window and optional filters (capacity, building, equipment).
Step 2: Availability Service filters rooms by metadata: Using cached room data, the service narrows the candidate list to rooms matching the criteria.
Step 3: Availability check against cache: For each candidate room, the service checks Redis for booked intervals on the requested date.
Step 4: Cache miss triggers database query: For rooms not in cache, the service queries PostgreSQL, populates the cache, and checks availability.
Step 5: Results returned: The list of available rooms is returned to the client, sorted by relevance (closest building, best capacity match).
Define the data model. Identify the main entities, their attributes, and relationships. Consider the choice of database type (SQL vs NoSQL) and justify your decision based on access patterns...
The data model centers on two tables: rooms and bookings. The interesting decision is not the schema itself but how the booking table enforces no-overlap constraints, and why PostgreSQL is the right choice for a system where correctness matters more than write throughput.
This system needs ACID transactions to prevent double-bookings. PostgreSQL provides row-level locking (SELECT FOR UPDATE), serializable transaction isolation, and exclusion constraints that can enforce non-overlapping time ranges at the database level. NoSQL databases like DynamoDB or Cassandra cannot provide these guarantees without significant application-level coordination.
rooms
id BIGSERIAL PRIMARY KEY
name VARCHAR(255) NOT NULL
building VARCHAR(100) NOT NULL
floor INT NOT NULL
capacity INT NOT NULL
equipment JSONB DEFAULT '[]'
status VARCHAR(20) DEFAULT 'active'
created_at TIMESTAMP DEFAULT NOW()
The equipment field uses JSONB to store a flexible list of amenities (projector, video conferencing, whiteboard) without requiring a separate join table. PostgreSQL's JSONB operators support efficient containment queries: "find rooms with both projector AND video conferencing."
bookings
id BIGSERIAL PRIMARY KEY
room_id BIGINT REFERENCES rooms(id)
user_id BIGINT REFERENCES users(id)
start_time TIMESTAMP NOT NULL
end_time TIMESTAMP NOT NULL
title VARCHAR(255)
status VARCHAR(20) DEFAULT 'confirmed'
idempotency_key VARCHAR(255) UNIQUE
created_at TIMESTAMP DEFAULT NOW()
INDEX idx_bookings_room_time ON bookings(room_id, start_time, end_time)
INDEX idx_bookings_user ON bookings(user_id, start_time)
The composite index on (room_id, start_time, end_time) is the most important index in the entire system. Every availability check and every double-booking prevention query uses it. Without this index, the system scans the entire bookings table for every request.
booking_participants
booking_id BIGINT REFERENCES bookings(id)
user_id BIGINT REFERENCES users(id)
PRIMARY KEY (booking_id, user_id)
users
id BIGSERIAL PRIMARY KEY
name VARCHAR(255) NOT NULL
email VARCHAR(255) UNIQUE NOT NULL
role VARCHAR(20) DEFAULT 'user'
PostgreSQL offers a powerful alternative to application-level locking: the exclusion constraint with the btree_gist extension.
sql
ALTER TABLE bookings ADD CONSTRAINT no_overlapping_bookings
EXCLUDE USING gist (
room_id WITH =,
tsrange(start_time, end_time) WITH &&
)
WHERE (status = 'confirmed');
This constraint tells PostgreSQL: "no two confirmed bookings for the same room can have overlapping time ranges." The database enforces this at the storage level, making double-booking physically impossible regardless of application logic bugs.
Key: room_avail:{room_id}:{date}
Value: Sorted set of booked time ranges
TTL: 24 hours (auto-expire past dates)
Redis stores availability data as sorted sets of booked intervals per room per day. Checking availability means querying the sorted set for overlapping ranges, an O(log N) operation where N is the number of bookings for that room on that day (typically under 10).
Deep dive into 2-3 key components. Explain how they work, how they scale, discuss tradeoffs, capacity, and any relevant algorithms or data structures.
The core challenge of this system is not scale but correctness: preventing two bookings from overlapping on the same room. This section dives deep into the concurrency control mechanisms, the availability query engine, and the cache synchronization strategy.
This is the single most important component in the system. When two users simultaneously try to book Room A from 2pm to 3pm, exactly one must succeed. There are three approaches, each with different trade-offs.
Double-booking prevention: two concurrent requests hit SELECT FOR UPDATE, the first acquires the lock and books successfully, the second waits then fails with a conflict.
Approach 1: Pessimistic Locking (SELECT FOR UPDATE)
The booking service begins a transaction and runs SELECT FOR UPDATE on the bookings table for the target room and overlapping time range. This acquires a row-level lock that blocks any other transaction from reading or modifying those rows until the first transaction commits or rolls back.
If no conflicting booking exists, the service inserts the new booking and commits. The second user's transaction, which was blocked on the lock, now runs its SELECT and finds the newly committed booking, returning a conflict error.
This approach is simple, reliable, and the recommended default. The downside is that blocked transactions hold database connections while waiting, but at 70 writes/sec this is not a concern.
Approach 2: Optimistic Concurrency (Version Check)
The service reads the current state without locks, prepares the booking, and attempts to INSERT with a version check condition. If another booking was inserted between read and write, the INSERT fails and the user retries. This avoids holding locks but increases retry frequency for popular rooms during peak times.
Approach 3: Database Exclusion Constraint
PostgreSQL's exclusion constraint makes double-booking physically impossible at the storage level. The application attempts an INSERT and catches any constraint violation.
The best practice is defense in depth: use pessimistic locking for clean error handling, and the exclusion constraint as a safety net against application bugs.
Key Insight
In an interview, start with pessimistic locking because it is the most intuitive and correct. Then mention the exclusion constraint as a defense-in-depth addition. If the interviewer asks about high contention, discuss optimistic concurrency as an alternative. This progression shows you understand trade-offs, not just solutions.
The availability query needs to answer: "which rooms are free between 2pm and 3pm on Tuesday, with capacity for 8 people, in Building A?"
Room search flow: Availability Service checks Redis cache first, falls back to PostgreSQL on miss, then populates the cache.
Cache-first approach: The Availability Service checks Redis for the room's booked intervals on the requested date. If found, it compares the requested time range against booked intervals locally, an O(log N) operation. No database query needed.
Cache-miss fallback: On a cache miss, the service queries PostgreSQL for all confirmed bookings for the room on that date, populates Redis, and then performs the availability check. The next request for the same room and date hits cache.
Filtering pipeline: Room metadata (building, floor, capacity, equipment) is filtered first (from cached room data), then availability is checked only for matching rooms. This reduces the number of availability lookups significantly. If the user filters for "Building A, Floor 3, 8+ capacity," only a handful of rooms need availability checking.
Cache invalidation: after a booking commits, the Booking Service deletes the affected room-date cache entry. The next availability query rebuilds it from PostgreSQL.
When a booking is created, updated, or cancelled, the Booking Service deletes the affected cache entry (room_id + date). The next availability query triggers a cache miss and rebuilds from the database.
Why delete instead of update? The Booking Service would need to know the exact cache format and compute new availability, which tightly couples the services. Deletion is simpler: let the Availability Service own the cache format and rebuild on demand.
Each booking request includes an idempotency key (hash of user_id + room_id + start_time). The bookings table has a unique constraint on idempotency_key. If a client retries due to a network timeout, the INSERT fails with a duplicate key error, and the server returns the existing booking instead of creating a duplicate.