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...
Here's the API that allows users to browse/search available rooms:
GET v1/view_available_rooms {
user_id: UUID,
start_time: Timestamp,
end_time: Timestamp,
capacity: Integer,
building: String,
floor: String,
equipment: String,
}
For creating room reservations:
PUT v1/create_reservation {
room_id: UUID,
start_time: Timestamp,
end_time: Timestamp,
organizer: String,
participants: List
}
For updating room reservations:
POST v1/update_reservation {
reservation_id: UUID,
start_time: Timestamp,
end_time: Timestamp,
organizer: String,
participants: List
}
For canceling room reservations:
DELETE v1/delete_reservation {
reservation_id: UUID,
start_time: Timestamp,
end_time: Timestamp,
organizer: String,
participants: List
}
For viewing room inventories:
GET v1/view_room_inventory {
admin_user_id: UUID,
building_id: UUID
}
For managing rooms:
POST v1/manage_room {
admin_user_id: UUID,
building_id: UUID,
room_id: UUID,
operation_type: String
}
For sending notifications to users:
POST v1/send_notification {
user_id: UUID,
notification_type: String
}
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.
Let's walk through the several flows.
First, for any requests sent from the client to the system, we go through an API gateway that does 2 things: rate limiting and authentication. For authentication, we call the access control service, to determine whether this is a normal employee account or a manager account. Once authenticated, the server sends back a JWT token, for the user to send in following requests.
Let's also talk about the data models. There are 3 relevant tables in the relational database:
reservations table {
reservation_id: UUID
room_id: UUID,
start_time: Timestamp,
end_time: Timestamp,
organizer: String,
organizer_user_id: UUID,
participants: List
participants_user_id: List
}
rooms table {
room_id: UUID,
capacity: Integer,
building: String,
floor: String,
equipment: String,
existing_reservations: List
}
users table {
user_id: UUID,
user_name: String,
user_role: String,
}
Here we use a distributed relational database, because the schema doesn't change frequently (no need for document database), the write throughput is small (peak write QPS is only around 2), and we need complex join queries from multiple tables. We also need strong ACID guarantees. Under the circumstance, a relational db works much better than a noSQL database. The tradeoff here is if the throughput of the system increases, the complexity of doing horizontal scaling is large. Also, the write latency is much higher than noSQL databases.
Let's then talk about the write path. When users want to create/edit/update an reservation, the request gets routed to reservation service.
For creating an reservation, the user pulls the relevant room data from rooms table and the relevant user data from users table, and then write a new row to the reservations table. While doing so, we also trigger a "reservation created" event that will be put on the Kafka message queue. The message gets consumed by notification service, which sends requests to the email notification server and app notification server, and send a reminder to the user. Another event is also created to send notification 30 minutes before a meeting starts.
For deleting an reservation, the reservation service deletes the corresponding row from the reservation table based on the reservation_id.
For editing an reservation, the reservation table reads the current reservation data, and makes the corresponding edits.
We need to ensure strong consistency for reservations, since it's an inventory problem and has strong requirements for consistency. When 2 users want to create a reservation for the same room concurrently for the same time period, the first user to submit the request acquires a distributed lock that looks like "lock:room:101:2026-06-03T14:00-17:00". If the second request overlaps with the time requested by the first request, it got rejected immediately. However, the distributed lock is only valid for 30 seconds. Otherwise, we fail the reservation booking request, then releases the lock on the row. The tradeoff here is higher write latency, and higher operational complexity.
For the read path, when users view browse and search available rooms, we filter rooms by user's selections of time slot, capacity, location (building/floor), and equipment. The reservation service reads from the rooms table based on the filtering. Given the peak read QPS is around 5, we don't need database sharding/replicas, or a caching layer at the moment. However, we still want to add a redis cluster as caching layer between reservation service and the postgres db, and serve repeated requests from the cache. This will ensure fast serving of available rooms.
Inside the cache, we will cache:
key: date-building_id-room_id,
value: available time ranges for a date
and
key: date-room_id
value: date-room_id
This should address the frequent queries made by the user, which should greatly decrease read latency and increase read availability. When users want to see available rooms in building B for a specific date, we can quickly query the cache to see what rooms are available for the requested timeslot.
It will be a write-through cache, where each write to the cache is synchronously propagated to the database. With this implementation, the write latency is higher, but we focus on maintaining consistency in our design. The cache should always return non-stale data. Whenever a new reservation is created, the available time ranges for a date for a room is updated in the cache.
In case of cache miss, we will fall back to the database for all reads. In case of cache crash, we will also defer to the database for handling all traffic. This will increase read latency during that time. To prevent the database from being overwhelmed, we could do:
Deep dive into 2-3 key components. Explain how they work, how they scale, discuss tradeoffs, capacity, and any relevant algorithms or data structures.
Let's walk through the several flows.
First, for any requests sent from the client to the system, we go through an API gateway that does 2 things: rate limiting and authentication. For authentication, we call the access control service, to determine whether this is a normal employee account or a manager account. Once authenticated, the server sends back a JWT token, for the user to send in following requests.
Let's also talk about the data models. There are 3 relevant tables in the relational database:
reservations table {
reservation_id: UUID
room_id: UUID,
start_time: Timestamp,
end_time: Timestamp,
organizer: String,
organizer_user_id: UUID,
participants: List
participants_user_id: List
}
rooms table {
room_id: UUID,
capacity: Integer,
building: String,
floor: String,
equipment: String,
existing_reservations: List
}
users table {
user_id: UUID,
user_name: String,
user_role: String,
}
Here we use a distributed relational database, because the schema doesn't change frequently (no need for document database), the write throughput is small (peak write QPS is only around 2), and we need complex join queries from multiple tables. We also need strong ACID guarantees. Under the circumstance, a relational db works much better than a noSQL database. The tradeoff here is if the throughput of the system increases, the complexity of doing horizontal scaling is large. Also, the write latency is much higher than noSQL databases.
Let's then talk about the write path. When users want to create/edit/update an reservation, the request gets routed to reservation service.
For creating an reservation, the user pulls the relevant room data from rooms table and the relevant user data from users table, and then write a new row to the reservations table. While doing so, we also trigger a "reservation created" event that will be put on the Kafka message queue. The message gets consumed by notification service, which sends requests to the email notification server and app notification server, and send a reminder to the user. Another event is also created to send notification 30 minutes before a meeting starts.
For deleting an reservation, the reservation service deletes the corresponding row from the reservation table based on the reservation_id.
For editing an reservation, the reservation table reads the current reservation data, and makes the corresponding edits.
We need to ensure strong consistency for reservations, since it's an inventory problem and has strong requirements for consistency. When 2 users want to create a reservation for the same room concurrently for the same time period, the first user to submit the request acquires a distributed lock that looks like "lock:room:101:2026-06-03T14:00-17:00". If the second request overlaps with the time requested by the first request, it got rejected immediately. However, the distributed lock is only valid for 30 seconds. Otherwise, we fail the reservation booking request, then releases the lock on the row. The tradeoff here is higher write latency, and higher operational complexity.
For the read path, when users view browse and search available rooms, we filter rooms by user's selections of time slot, capacity, location (building/floor), and equipment. The reservation service reads from the rooms table based on the filtering. Given the peak read QPS is around 5, we don't need database sharding/replicas, or a caching layer at the moment. However, we still want to add a redis cluster as caching layer between reservation service and the postgres db, and serve repeated requests from the cache. This will ensure fast serving of available rooms.
Inside the cache, we will cache:
key: date-building_id-room_id,
value: available time ranges for a date
and
key: date-room_id
value: date-room_id
This should address the frequent queries made by the user, which should greatly decrease read latency and increase read availability. When users want to see available rooms in building B for a specific date, we can quickly query the cache to see what rooms are available for the requested timeslot.
It will be a write-through cache, where each write to the cache is synchronously propagated to the database. With this implementation, the write latency is higher, but we focus on maintaining consistency in our design. The cache should always return non-stale data. Whenever a new reservation is created, the available time ranges for a date for a room is updated in the cache.
In case of cache miss, we will fall back to the database for all reads. In case of cache crash, we will also defer to the database for handling all traffic. This will increase read latency during that time. To prevent the database from being overwhelmed, we could do: