Machine coding · Booking and reservations
Meeting calendar application
- Reported once
- SDE-1
- Observer
- Strategy
- 1 source
One candidate write-up. Treat it as a single data point.
Build a calendar application that schedules meetings, shows each user's calendar, and lets invitees accept or reject. The report lists seven requirements, not all of which are readable, and the code had to run cleanly at the end, so the answer states the assumed requirements.
Restated in our own words, not a quote from Swiggy or any page. The answer below is written for this site.
Clarify first
Pin the scope down before you name a single class.
Since the full requirement list is not known, confirm it:
- Can a meeting have several invitees, and does the organizer count as attending automatically?
- What does a user's calendar show: only accepted meetings, or pending ones too?
- Is an overlap with another accepted meeting an error, or just a warning?
- Who can cancel a meeting?
- Do users need to be told about invites and replies?
- Are there recurring meetings or time zones?
Assumed scope. The report names scheduling, a calendar per user and accept or reject. The rest is this answer's assumption. In: users, a meeting with a time range and invitees, accept and reject, a per-user calendar view over a time range, overlap detection with a configurable policy, notifications, and cancel by the organizer. Out: recurrence, time zones, rooms and persistence. Times are integer minutes.
Entities and relationships
Who owns what, and who talks to whom.
- User has an id and a name.
- Meeting has an organizer, a title, a start and an end, and a response for each person: ACCEPTED, PENDING or REJECTED. It answers
overlaps(other). - CalendarService has many users and many meetings, uses one conflict policy, and notifies observers. It exposes
schedule,respond,cancelandview. - StrictPolicy and PermissivePolicy implement the conflict policy: given the conflicting meetings, say whether the action is allowed.
- InboxObserver implements the observer interface. It receives events and keeps a message list per user.
The design
The class map, the main flow and the patterns that earn their place.
- A client schedules a meeting or answers an invite.
- The service builds or updates the meeting, which stores one response per person.
- It finds the user's accepted overlapping meetings and asks the policy whether to proceed.
- Each change is sent to every subscribed observer, for example an inbox per user.
Open on the whiteboardAll 2 diagrams on one tidy board, with a step-by-step walkthrough.
The service is the single entry point. It keeps meetings in a map and keeps each person's response inside the meeting, which means there is one source of truth and no per-user copy to keep in sync.
Overlap rule. Two meetings overlap when each starts before the other ends, using half-open ranges. A meeting that ends at 11:00 and one that starts at 11:00 do not conflict, which is the boundary an interviewer will test. conflictsFor(user, meeting) returns the ids of the user's accepted meetings that overlap. Only accepted meetings block time, so pending and rejected ones never do.
Strategy. What to do about a conflict is a policy, not a fact. StrictPolicy refuses when there is any conflict and PermissivePolicy allows it. The service passes the conflict list to policy.allows both when scheduling (for the organizer) and when an invitee accepts. Swapping the policy changes behaviour without touching the service.
Observer. The service does not know how anyone is notified. It emits events (invited, accepted, rejected, cancelled) to every subscribed observer. The inbox observer is one subscriber; email or push would be others. Events are delivered synchronously here, so a slow subscriber would block the caller. A subscriber that throws would also stop the notifications after it. A real system hands events to a queue.
Flows. schedule validates the range and the users, builds the meeting with the organizer accepted and every invitee pending, runs the policy check, stores it and emits an invite per invitee. respond checks that the person is an invitee, runs the policy check on acceptance, records the answer and tells the organizer. view returns meetings in a time range where the user is not rejected, sorted by start, with ties kept in the order the meetings were created.
Trade-off. conflictsFor and view scan all meetings. For a real calendar, index each user's meetings by start time and search only the neighbours of the new range.
The code
Runnable JavaScript, with the checks it passes.
class User {
constructor(id, name) {
this.id = id;
this.name = name;
}
}
class Meeting {
constructor(id, organizerId, title, start, end, inviteeIds) {
this.id = id;
this.organizerId = organizerId;
this.title = title;
this.start = start;
this.end = end;
this.responses = new Map([[organizerId, "ACCEPTED"]]);
for (const userId of inviteeIds) this.responses.set(userId, "PENDING");
}
overlaps(other) {
return this.start < other.end && other.start < this.end;
}
}
class StrictPolicy {
allows(conflicts) {
return conflicts.length === 0;
}
}
class PermissivePolicy {
allows() {
return true;
}
}
class InboxObserver {
constructor() {
this.inboxes = new Map();
}
update(event) {
if (!this.inboxes.has(event.userId)) this.inboxes.set(event.userId, []);
this.inboxes.get(event.userId).push(event.type + ":" + event.meetingId);
}
inbox(userId) {
return this.inboxes.get(userId) || [];
}
}
class CalendarService {
constructor(policy) {
this.policy = policy;
this.users = new Map();
this.meetings = new Map();
this.observers = [];
this.nextId = 1;
}
addUser(user) {
this.users.set(user.id, user);
}
subscribe(observer) {
this.observers.push(observer);
}
emit(type, meeting, userId) {
for (const observer of this.observers) observer.update({ type, meetingId: meeting.id, userId });
}
conflictsFor(userId, meeting) {
const found = [];
for (const other of this.meetings.values()) {
const accepted = other.responses.get(userId) === "ACCEPTED";
if (accepted && other.id !== meeting.id && other.overlaps(meeting)) found.push(other.id);
}
return found;
}
schedule(organizerId, title, start, end, inviteeIds) {
if (!this.users.has(organizerId)) return { ok: false, reason: "unknown organizer" };
if (!(start < end)) return { ok: false, reason: "invalid time range" };
const invitees = [...new Set(inviteeIds)].filter((id) => id !== organizerId);
if (invitees.some((id) => !this.users.has(id))) return { ok: false, reason: "unknown invitee" };
const meeting = new Meeting("M" + this.nextId, organizerId, title, start, end, invitees);
const conflicts = this.conflictsFor(organizerId, meeting);
if (!this.policy.allows(conflicts)) return { ok: false, reason: "organizer is busy", conflicts };
this.nextId++;
this.meetings.set(meeting.id, meeting);
for (const id of invitees) this.emit("invited", meeting, id);
return { ok: true, meeting };
}
respond(meetingId, userId, accept) {
const meeting = this.meetings.get(meetingId);
if (!meeting || !meeting.responses.has(userId) || userId === meeting.organizerId) {
return { ok: false, reason: "not an invitee" };
}
if (accept) {
const conflicts = this.conflictsFor(userId, meeting);
if (!this.policy.allows(conflicts)) return { ok: false, reason: "overlaps an accepted meeting", conflicts };
}
meeting.responses.set(userId, accept ? "ACCEPTED" : "REJECTED");
this.emit(accept ? "accepted" : "rejected", meeting, meeting.organizerId);
return { ok: true };
}
cancel(meetingId, userId) {
const meeting = this.meetings.get(meetingId);
if (!meeting || meeting.organizerId !== userId) return false;
this.meetings.delete(meetingId);
for (const id of meeting.responses.keys()) if (id !== userId) this.emit("cancelled", meeting, id);
return true;
}
view(userId, from, to) {
const rows = [];
for (const m of this.meetings.values()) {
const status = m.responses.get(userId);
if (!status || status === "REJECTED") continue;
if (m.start < to && from < m.end) rows.push({ id: m.id, title: m.title, start: m.start, end: m.end, status });
}
return rows.sort((a, b) => a.start - b.start);
}
}
function createService(policy) {
const service = new CalendarService(policy);
for (const id of ["ann", "bob", "cy"]) service.addUser(new User(id, id));
const inbox = new InboxObserver();
service.subscribe(inbox);
return { service, inbox };
}Runs here in your browser, in a sandboxed worker. Nothing is sent anywhere.
Try your own input
Write JavaScript that calls the code above and prints with console.log. It runs after the solution.
Extend it
The change requests an interviewer throws next.
- How would you support recurring meetings, and how does an overlap check work against an infinite series?
- How would you add time zones without changing the overlap rule?
- How would you let a user find a free slot that works for every invitee?
- How would you reserve a meeting room, and how does that clash differ from a person's clash?
- How would you deliver notifications reliably when the subscriber is slow or fails?
Where candidates slip
The mistakes this problem punishes.
What this problem punishes:
- Using closed ranges, so back-to-back meetings are reported as conflicts.
- Storing a separate meeting copy per user, which drifts out of sync when one person replies.
- Letting pending or rejected meetings block time.
- Hard-coding what happens on a conflict instead of making it a policy, then having no room for the follow-up.
- Calling the notification code directly from the scheduling code, so every new channel edits the service.
- Not checking that only invitees can respond and only the organizer can cancel.
- Running out of time with an unfinished large design. A smaller version that runs cleanly is the better result.
Have a go before you read.
Interviews reward the sketch you make cold. Clarify out loud, draw it, then compare with the answer.
Now make it stick
Where this was reported
Public pages only. Companies do not confirm these, and they rotate their questions, so treat each link as one person's account or one list's tag.