Problem Statement
An office has several meeting rooms. Some are small, some are big, some have a projector. People want to book a room for a stretch of time, like 9:00 to 10:00 on Monday. The system must never let two meetings hold the same room at the same time. It should also help people find a free room that suits them, and let them cancel.
It sounds easy and it is, until you hit the two spots interviewers care about. First, what exactly counts as "overlapping"? Second, what happens when two people click Book on the same room at the same moment?
Questions to Ask First
| Question | Assumption I will make |
|---|---|
| One building or many? | One office. A room has an id, a name, a capacity and a set of features. |
| Can meetings run back to back? | Yes. A meeting ending at 10:00 and one starting at 10:00 is fine. |
| Minimum or maximum length? | None. Any start before end. |
| Who can cancel? | Only the person who booked it. |
| Recurring meetings? | Weekly, for a given number of weeks. All or nothing. |
| What happens when the room is taken? | The booking is rejected. No waiting list. |
| Is the data stored on disk? | No. Everything is in memory. |
| Time zones? | One zone. I use LocalDateTime and say so out loud. |
Requirements
- Add rooms with a capacity and a set of features (projector, whiteboard, video conference).
- Book a specific room for a time interval.
- Reject a booking that overlaps an existing one in the same room.
- Search for rooms that are free for an interval, have enough seats and have the features asked for.
- Pick a room automatically, with the picking rule easy to swap. The default is the smallest room that fits.
- Cancel a booking.
- Book a weekly series. If any week clashes, nothing is booked.
- Two people booking the same room at the same time: exactly one wins.
Core Classes
Nine source files plus Main. Most of them are tiny records, which are plain immutable data holders. The real work is in TimeSlot (the overlap rule), RoomCalendar (one sorted calendar and one lock per room) and BookingService (the front door).
How the Design Works
The overlap rule
A booking covers a stretch of time, written as start and end. I treat the end as not included. A meeting from 9:00 to 10:00 uses the room up to the moment 10:00 arrives, and 10:00 itself belongs to whoever comes next. Maths people call this a half-open interval. It is the reason back to back meetings work.
Here is a picture. Say Maple already has 9:00 to 10:00 booked.
time: 08:00 09:00 10:00 11:00
| | | |
existing: [=======)
new A 08:00-09:00 [=====) fine, touches at 9:00
new B 08:30-09:30 [========) clash
new C 09:15-09:45 [==) clash, sits inside
new D 08:00-12:00 [===============) clash, covers it
new E 09:30-10:30 [=======) clash
new F 10:00-11:00 [=====) fine, touches at 10:00The easiest way to find the rule is to ask when two meetings do not overlap. Only two ways: the new one ends before (or exactly when) the old one starts, or the new one starts after (or exactly when) the old one ends. Flip that around and you get the one line:
public boolean overlaps(TimeSlot other) {
return start.isBefore(other.end) && other.start.isBefore(end);
}Read it aloud: "I start before you end, and you start before I end." Check it on case C: new starts at 9:15, which is before the old end 10:00. Old starts at 9:00, which is before the new end 9:45. Both true, so they overlap. Check F: new starts 10:00, which is not before old end 10:00. So false. Touching is fine.
Many people write four or five if branches for "starts inside", "ends inside", "covers" and so on. The single line covers all of them. Memorise it.
One sorted calendar per room
Each room has its own RoomCalendar. It holds bookings in a TreeMap, keyed by start time. A TreeMap keeps its keys sorted, and it can answer "what is the nearest key at or before X?" quickly.
Why does that help? The slow way to check a new booking is to loop over every booking in the room and call overlaps. With 1,000 bookings that is 1,000 checks every time. The calendar never has overlapping bookings, so they sit on the timeline in a clean row, one after another. That means only two neighbours can possibly clash with a new one: the booking that starts at or just before the new start, and the booking that starts just after it.
Why only those two? Say an earlier booking, further back, reached past the new start. Then it would also overlap the one sitting between them, and that cannot be, because the calendar never holds overlaps. So the nearest one on each side is enough.
public synchronized boolean isFree(TimeSlot slot) {
Map.Entry<LocalDateTime, Booking> before = byStart.floorEntry(slot.start());
Map.Entry<LocalDateTime, Booking> after = byStart.higherEntry(slot.start());
return !clashes(before, slot) && !clashes(after, slot);
}floorEntry gives the booking starting at or before the new start. higherEntry gives the first booking starting after it. Two lookups, each costing about log n steps, so roughly 10 steps for 1,000 bookings instead of 1,000. For one meeting room per day it makes no difference, I will admit. For a year of bookings across a campus it does, and interviewers like to hear the reason.
Avoiding double booking: a lock per room
The trap is "check, then insert". Two threads both ask "is Maple free at 9:00?", both hear "yes", both insert. Now Maple is double booked, and no single line of code was wrong.
The fix is to make the check and the insert one step that nobody can cut into. In Java the simplest way is synchronized. Every method of RoomCalendar is synchronized, so each calendar object is its own lock:
public synchronized boolean addAll(List<Booking> bookings) {
List<Booking> added = new ArrayList<>();
for (Booking booking : bookings) {
if (!isFree(booking.slot())) {
added.forEach(this::remove);
return false;
}
byStart.put(booking.slot().start(), booking);
added.add(booking);
}
return true;
}Why one lock per room and not one lock for the whole system? Think of the keys to each room hanging on its own hook. If there were one key for the whole building, a person booking Oak would make a person booking Cedar wait for no reason. With a lock per room, only people fighting over the same room take turns. Different rooms run at the same time.
Could you do it without locks? You could use a single ConcurrentSkipListMap with compare-and-swap tricks, but checking two neighbours and then inserting is still two steps. I would not try this in an interview. A plain lock is the honest answer.
The search method findFreeRooms also calls isFree, but it takes the lock only for a moment per room. Its answer can be out of date a millisecond later. So bookAny never trusts it. It walks down the ranked list and tries a real book on each room until one succeeds:
public Optional<Booking> bookAny(BookingRequest request) {
for (Room room : findFreeRooms(request)) {
Optional<Booking> booking = book(room.id(), request.organizer(), request.slot());
if (booking.isPresent()) {
return booking;
}
}
return Optional.empty();
}If someone grabs the best room between the search and the booking, this loop quietly moves to the next room.
Picking the room: a strategy
Different offices want different rules. Smallest room that fits, biggest room, closest to my floor, least used. So the rule lives behind a one-method interface:
@FunctionalInterface
public interface RoomSelectionStrategy {
List<Room> rank(List<Room> freeRooms);
}The service first filters to rooms that have enough seats, have the features, and are free. Then the strategy sorts that list, best first. The default one:
public List<Room> rank(List<Room> freeRooms) {
return freeRooms.stream()
.sorted(Comparator.comparingInt(Room::capacity).thenComparing(Room::id))
.toList();
}Why return a ranked list instead of one room? Because of the race above. If the strategy picked only one room and that room was just taken, we would have nothing to fall back on. The tie break on id makes the result the same every run.
Smallest room that fits is a sensible default. It keeps the big rooms free for the big meetings. Booking a 30 seat hall for two people is the thing that annoys everyone.
Recurring bookings
A weekly meeting is just several bookings made together. bookWeekly builds them by shifting the first slot by 0, 1, 2 ... weeks, then hands the whole list to addAll. Because addAll runs under the room lock and takes back anything it added if one week clashes, you get all or nothing, and nobody can sneak a booking in halfway.
The service keeps no "series" object. Each week is an ordinary booking. So you can cancel one week with no extra code. Cancelling the whole series would need a series id, and I list that under follow ups.
Cancellation
public boolean cancel(String bookingId, String requestedBy) {
Booking booking = bookingsById.get(bookingId);
if (booking == null || !booking.organizer().equals(requestedBy)) {
return false;
}
if (!calendarOf(booking.roomId()).remove(booking)) {
return false;
}
bookingsById.remove(bookingId);
return true;
}Only the organizer can cancel. Removing from the TreeMap is by start time, and the remove(key, value) form only deletes if the stored booking is the same one, so a stale cancel cannot delete somebody else's booking that took the same time later.
Patterns Used and Why
Strategy for choosing a room. The rule changes more often than anything else, so it is the thing to keep separate. The service never mentions "smallest".
That is the only classic pattern here. The rest is ordinary good habits: records for data that should not change, one class for each job, and the lock hidden inside RoomCalendar so callers never see it.
I did not add a Builder for BookingRequest. A record with four fields is fine. I did not add a Singleton either. The service is created once in main and passed around. A hidden global makes tests hard.
Walk Through One Full Example
The demo uses four rooms: Oak (4 seats, whiteboard), Maple (8, projector and whiteboard), Cedar (12, projector, video, whiteboard) and Atrium (30, projector and video). All dates are in March 2030, and Monday is March 4.
Real output:
== Overlap rules in Maple ==
Asha 09:00-10:00 -> booked B1 maple Mar 4 09:00-10:00 by Asha
Ben 10:00-11:00 -> booked B2 maple Mar 4 10:00-11:00 by Ben
Chitra 09:30-10:30 -> rejected
Dev 08:00-12:00 -> rejected
Esha 09:15-09:45 -> rejected
== Search: 6 people, projector, 09:30-10:30 ==
free rooms, smallest first: [Cedar, Atrium]
bookAny -> booked B6 cedar Mar 4 09:30-10:30 by Farah
same search with a biggest-first strategy: [Atrium, Cedar]
== Cancellation ==
Chitra cancels Asha's booking B1: false
Ben cancels his own booking B2: true
Gita 10:00-11:00 -> booked B7 maple Mar 4 10:00-11:00 by Gita
== Weekly standup in Oak, 4 weeks ==
Hari one-off in week 3 -> booked B8 oak Mar 18 09:00-09:30 by Hari
Ira standup at 09:00 booked 0 bookings, Oak has 1
Ira standup at 11:00 booked 4 bookings, Oak has 5
B13 oak Mar 4 11:00-11:15 by Ira
B14 oak Mar 11 11:00-11:15 by Ira
B8 oak Mar 18 09:00-09:30 by Hari
B15 oak Mar 18 11:00-11:15 by Ira
B16 oak Mar 25 11:00-11:15 by Ira
== Concurrency ==
8 threads, same room, same slot: 1 booked, 7 rejected
5 threads, any room (only Cedar and Atrium fit): 2 booked, 3 rejectedWalking through it:
- Asha takes Maple 9:00 to 10:00. Ben takes 10:00 to 11:00. They touch at 10:00 and that is fine.
- Chitra (overlaps the end of Asha's), Dev (swallows Asha's whole meeting) and Esha (sits inside it) are all rejected. Three different shapes, one rule.
- Farah needs a room for 6 with a projector at 9:30. Oak has no projector and only 4 seats. Maple has both but is busy. That leaves Cedar and Atrium, and the smallest-first rule puts Cedar on top. A different strategy, passed in as a lambda, flips the order with no change to the service.
- Chitra tries to cancel Asha's booking and is refused. Ben cancels his own, and Gita immediately takes the freed hour.
- A one-off meeting sits in Oak in week 3. Ira's 9:00 weekly standup would hit it, so nothing at all is booked and Oak still has 1 booking. The 11:00 standup has no clash and all 4 weeks go in.
- In the concurrency part, a latch holds 8 threads at a starting line and releases them together. They all go for the same room and slot. Exactly one wins, every run. For the second test, 5 threads ask for any room for 10 people with a projector. Only Cedar and Atrium fit, so exactly 2 win.
About the ids. The jump from B2 to B6 is real: ids are taken from a counter before the clash check, so rejected attempts still use up a number. Ids are unique, but they have gaps. That is fine for ids. If you need gap-free numbers, hand them out only after a successful add.
Which thread wins the race is not printed because it changes between runs. The counts never change.
Edge Cases
- Back to back meetings are allowed, because the end is not included.
- Start equal to end, or start after end:
TimeSlotthrowsIllegalArgumentException. - A new booking that fully contains an old one, or sits fully inside one, is caught by the same rule.
- A booking that starts at exactly the same time as an existing one.
floorEntryreturns that booking (floor includes an equal key), so it is caught. - Unknown room id: throws
IllegalArgumentExceptioninstead of returning a vague "false". - Cancel something that is already gone, or cancel by the wrong person: returns
false. - Cancel and re-book the same slot straight away: works, the slot is free again.
- A weekly series where week 3 clashes: nothing is booked, not even weeks 1 and 2.
- A meeting that crosses midnight is fine, because
TimeSlotuses full date and time. - Search finds no room:
bookAnyreturns an emptyOptional.
Follow Up Questions an Interviewer May Ask
"Cancel a whole recurring series?"
Give every booking in a series the same
seriesIdand addcancelSeries(seriesId). Cancelling "this and all later ones" is the same with a date filter.
"What about a recurring rule like "every weekday" or "first Monday of the month"?"
Pull the rule into its own small type that produces a list of slots from a start date.
bookWeeklyalready takes a list of bookings, so only the part that builds the list changes.
"Why a TreeMap and not a plain list?"
Lists need a full scan to check overlap. A
TreeMapjumps to the two neighbours. For a tiny room it hardly matters, and I would say that. It matters when there are many bookings per room.
"What if the whole system has millions of bookings?"
Then it is a database. A booking table with a room id, start and end, plus a constraint that stops overlaps (some databases can enforce this directly), or an indexed range query inside a transaction. The thinking is the same: check and insert must be atomic.
"How do you handle two servers?"
A Java lock only works inside one process. You need the database to be the referee, with a transaction or a unique constraint, or a distributed lock per room. A per-room lock is still the right grain.
"How would you add rules like "max 2 hours" or "not outside 8 to 6"?"
A list of small validators run before
addAll. Each says yes or no. Same idea as the strategy.
"What if the first-choice room is taken at the last second?"
That is why
rankreturns a list andbookAnyloops. The search is advice, and the booking is the truth.
"How do you handle time zones?"
Store instants (
Instant, which is UTC) and convert for display. I usedLocalDateTimeto keep the demo readable, and I would change this first in a real system.
"Why is isFree synchronized if it only reads?"
TreeMapis not safe to read while another thread is changing it. Reads and writes must use the same lock. If reads vastly outnumber writes, a read-write lock is the next step.
Common Mistakes
- Using
<=in the overlap check. That makes back to back meetings clash. - Writing a long list of cases for the overlap instead of the one line.
- Checking for a clash and then inserting in two separate steps with no lock.
- One global lock for all rooms. It is correct but needlessly slow.
- Trusting the search result and booking without checking again.
- Putting the room choosing
if/elseinside the service instead of behind an interface. - Booking a recurring series one by one, so a clash in week 3 leaves weeks 1 and 2 booked.
- Storing the end time as inclusive in some places and exclusive in others.
Where This Is Simplified
Everything is in memory, so a restart loses all bookings. There is one time zone. Rooms cannot be removed or changed once added. There is no waiting list, no notifications, and no "approval" step. Permissions are only "the organizer can cancel". The search scans all rooms, which is fine for tens of rooms and not for thousands. The id counter leaves gaps. And a lock inside one Java process is not a plan for several servers.
How to Run
From the repo root:
javac --release 17 -d /tmp/meetingroom-out $(find src/LLD/MeetingRoomBooking/src -name '*.java')
java -cp /tmp/meetingroom-out com.blueprint.lld.meetingroom.MainJava 17 or newer, no libraries. The files are in src/LLD/MeetingRoomBooking/src/com/blueprint/lld/meetingroom/.