The idea
What it is
“Design an elevator system.” Most candidates start drawing a car and a shaft. That is not where the difficulty is. A car that moves up and down is twenty lines.
The difficulty is that requests arrive faster than you can serve them, from two completely different places, and you have to decide two things every time: in what order do I serve what I have, and which car takes the next one. Everything else in this problem is scaffolding around those two decisions.
The whole system in three sentences
A hall call is (floor, direction) and goes to the ElevatorSystem, which asks a strategy which car should take it. A car call is (floor) and goes straight to one car. Each car then serves what it holds using LOOK: keep going the way you are going, stop for everything on the way, and only reverse when there is nothing left ahead.
What is actually being graded
- Are there two request types? A
HallCall(floor, direction)and aCarCall(floor). One list of integers is the single most common way to fail this round, and it is fatal — you cannot decide whether to stop for someone if you never recorded which way they wanted to go. - Is the serving order LOOK, not FIFO? Arrival order makes the car yo-yo past floors it is standing next to. The interviewer will hand you a request stream and count floors.
- Is which car a separate, swappable decision?
ElevatorSystem.request()asks aSchedulingStrategy. The car itself knows nothing about the other cars. - Is time an input? A
tick()you call from outside, or an injected clock. AThread.sleepinside the movement means nothing you wrote can be tested. - Does it run? Two cars, a stream of calls, and a
main()that prints floor-by-floor and then the total floors travelled.
Mechanics
How it works
Step 1 · Clarify — 5 minutes
- How many floors and how many cars? — “10 floors, 2 cars” is the sweet spot. One car removes the dispatch conversation, which is half the problem. Twenty cars is the same code with a bigger loop.
- Do riders enter a destination outside, or just a direction? — say just a direction. That is the classic building, and it is what makes hall calls and car calls different objects. Destination entry is the best follow-up you will get, so keep it in your pocket.
- Should I simulate time, or is this event-driven? — “I'll expose a
tick()that advances one floor, and drive it frommain().” Say this in the first five minutes and you have pre-answered the testability question. - Do cars have capacity limits? — ask, then say you will model a count and refuse riders, not requests. It is one field.
- Doors, motors, sensors, emergency brakes? — out of scope. Doors exist as a state with a timer, nothing more.
Step 2 · Two request types — and beginners merge them
Read the problem back and notice there are two buttons in two different places. The one in the lobby says “I am on floor 7 and I want to go up.” The one inside the car says “take me to floor 3.” They carry different information, they are created by different people, and they are answered by different objects.
The question that exposes it
“Your car is at floor 5 going up, and someone on floor 7 pressed the down button. Do you stop for them?” The answer is no — not on this trip. You carry on up, serve everyone going up, and pick them up on the way back down. If your requests are plain integers you cannot even express that answer, let alone code it.
Step 3 · FIFO is wrong, and you can measure how wrong
You have a car at floor 1. Four people press up buttons, in this order: floor 5, floor 2, floor 8, floor 3. Serve them in the order they arrived and watch what the car does.
floors travelled under each chip.The fix is called LOOK (the practical cousin of SCAN, the disk-arm algorithm). The rule is one sentence: the car has a direction; it keeps going that way, stopping for every request on the way, and only reverses when there is nothing left ahead of it.
Implementing that becomes easy the moment you hold requests in two sorted sets instead of one list.
TreeSet and a TreeSet(reverseOrder()); the code below is barely fifteen lines because the ordering is already done./** ONE floor of movement. Called from outside — there is no sleep in here. */
void step() {
if (state == CarState.DOORS_OPEN) { state = resume(); return; } // doors take one tick
if (!busy()) { direction = Direction.IDLE; state = CarState.IDLE; return; }
if (direction == Direction.IDLE) direction = towardsNearestRequest();
if (serveHere()) { state = CarState.DOORS_OPEN; return; } // already standing on one
if (!workAhead(direction)) direction = opposite(direction); // nothing left this way
if (serveHere()) { state = CarState.DOORS_OPEN; return; }
floor += (direction == Direction.UP) ? 1 : -1; // exactly one floor
floorsTravelled++;
state = (direction == Direction.UP) ? CarState.MOVING_UP : CarState.MOVING_DOWN;
if (serveHere()) state = CarState.DOORS_OPEN; // stop for it on the way
}
/** Remove a request at the current floor IF it matches where we are going. */
private synchronized boolean serveHere() {
if (direction == Direction.UP) {
if (upRequests.remove(floor)) return true;
if (!anyAbove(floor) && downRequests.remove(floor)) return true; // turnaround point
} else {
if (downRequests.remove(floor)) return true;
if (!anyBelow(floor) && upRequests.remove(floor)) return true;
}
return false;
}The two lines everyone forgets
The !anyAbove(floor) branches are the turnaround rule. A down request sitting at the top of the run must be served when the car gets there, otherwise the car flips direction, walks away from it, and comes back later. The other easy miss: a car call you pass on the way must be served. If your serveHere() only looks at the destination it was heading for, you have rebuilt FIFO with extra steps.
Step 4 · Which car goes — a separate, pluggable decision
So far every car knows how to serve what it holds. Nothing yet decides who holds what. That decision belongs to the ElevatorSystem, and it belongs behind an interface — because it is exactly the thing the interviewer will ask you to change.
A cost function you can defend out loud, in three cases: an idle car costs its distance. A car already moving your way, and past-you-is-ahead-of-it, also costs its distance — it will pass you anyway. A car moving away, or moving the wrong way, costs its distance plus a large fixed penalty, so it only wins when nothing else is remotely close.
Request splits the data; SchedulingStrategy splits the decision. Adding a third policy touches one new file and zero existing ones — that is Open/Closed (OCP) with a number attached. Notation: Class diagrams.Say this when you extract the strategy
“request() will not change when the policy does.” That sentence, plus a second implementation you can name — least-loaded, or zoned, or nearest-with-a-wait-time-cap — is worth more than a perfect cost function. It is the same seam Parking Lot used for spot allocation, and the interviewer is checking whether you spot it twice. Background: Strategy.
Step 5 · Time is an input, not something you sleep through
The single fastest way to make this program untestable is to write Thread.sleep(1000) inside the movement. Now a ten-floor trip takes ten real seconds, a test of the turnaround rule takes half a minute, and the interviewer cannot see anything happen before the round ends.
Instead expose tick(). One call advances every car by one floor. main() drives it in a loop, a test drives it three times and asserts a position, and a real building would drive it from a hardware timer. Same code, three clocks.
Step 6 · Concurrency — one lock, in one place
Hall calls arrive from ten floors at once, and a rider inside is pressing buttons at the same time. The request sets are shared state; two threads adding to a TreeSet at the same moment can corrupt it or lose an entry.
- Lock the queues, not the movement.
addRequest()and the removal insideserveHere()take the car's lock. Moving a floor and updating the counter do not need it. - One lock per car, not one global lock. Cars share nothing with each other, so a global lock would serialise a building for no reason.
- Dispatch reads car state without a lock, and that is fine — the strategy is picking a good car, not a provably optimal one. Say this: a slightly stale floor number costs you one floor of travel, not correctness.
- Never hold a lock across
step(). It is the same rule as brewing outside the lock in Coffee Machine: guard the arithmetic, not the work.
What the interviewer is checking
Not that you wrote a lock-free scheduler. Just that you can point at which field is shared and by whom, and that your answer is proportionate. “One lock per car around the two request sets” is the whole answer, and it takes ten seconds. More on the primitives in Locks, Mutex, Semaphore.
The follow-ups
- “Make one car an express that only stops at 1 and 6 to 10.” → give the car a
servesFloor(int)predicate and have the strategy return infinite cost for a call it cannot serve. No new class in the car, one guard in the cost function. - “Add a firefighter / service mode.” → a mode flag on the car that clears both request sets, refuses new ones, and takes a single destination. Cancelling every pending request is the part people forget — the riders waiting on floor 7 must be told, so the system re-dispatches their hall calls to another car.
- “Capacity and weight limits.” → a
riderCounton the car and a limit. Crucially it refuses riders, not requests: a full car still stops where it was going, it just does not let anyone in, and the hall call stays outstanding for the next car. - “Twenty cars in one bank.” → the strategy loop is already O(cars); nothing structural changes. What does change is that a purely nearest-car policy starts bunching cars together, so you add a zoning or least-load term. Both are new strategy classes.
- “Sixty floors, eight cars — what actually changes?” → destination dispatch. Riders type their destination on a lobby panel instead of pressing a direction, the system groups people going to similar floors into the same car, and hall calls disappear entirely.
HallCall(floor, direction)becomesTripRequest(from, to), and the strategy suddenly has enough information to be genuinely optimal. - “How would you test the scheduler?” → because time is a parameter, a test is: build a car, add three requests, call
step()five times, assert the floor and the remaining sets. No threads, no waiting. Say this unprompted.
Spending the 60 minutes
Direction,CarState,HallCall,CarCall(6 min) — the enums and the two request records. Tiny, and everything else leans on them.ElevatorCarwith the two sorted sets andaddRequest(6 min) — no movement yet.step()(18 min) — this is the round. Move one floor,serveHere(), flip when nothing is ahead. Print the floor every tick while you write it.ElevatorSystemwith one hardcoded “nearest car” rule (8 min) — get two cars moving from one request stream.- Extract
SchedulingStrategyand add a second implementation (8 min) — a five-minute refactor once it works, a rabbit hole if you start here. main()that prints a tick-by-tick trace and the total floors travelled (8 min) — the number at the bottom is what makes your design visible.
How this round is lost
- One
List<Integer> requests. Direction is gone, the “do you stop for the down button?” question cannot be answered, and no amount of clever scheduling on top recovers it. Thread.sleepinside the movement. A ten-floor trip now takes ten seconds, nothing can be tested, and the demo runs out of clock before it finishes.- Scheduling logic buried inside
ElevatorCar. The moment the interviewer says “now pick the least busy car instead”, you are editing the car class — and a second policy cannot exist alongside the first. - Forgetting the requests you pass. A car heading to floor 8 that sails past a waiting request at floor 5 has quietly reimplemented FIFO.
- Never reversing correctly. No turnaround rule means a request at the top of the run gets skipped, the car flips, and the person waits forever. That is starvation, and it is easy for an interviewer to construct.
- Designing a perfect cost function and never writing
step(). The cost function is worth one sentence; LOOK is worth the round.
Interactive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
A 10-floor building with two cars. Press ▲ or ▼ next to any floor to make a hall call — the explain line tells you which car took it and why. Press a floor number inside a car's panel for a car call. Then the whole point: press ⚡ Rush hour, run it under 🧭 LOOK, note floors travelled, press 🔀 FIFO, and run the identical burst again. Same requests, same cars, 11 floors versus 21. The 🎯 Nearest car and ⚖️ Least load chips swap the dispatch policy without changing a single call site.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
Make one hall call and read the reason
Press ▲ next to floor 7. The call line shows system.request(new HallCall(7, UP)) and one car flashes. The explain line does not just name the winner — it gives the cost for both cars. Note that the car chosen is the idle one, not necessarily the closest one in raw floors.
Press a button from inside a car
In the right-hand panel, click a floor number under car #1. The call line becomes car1.addRequest(new CarCall(3)) — a different object, going to a different place, and no strategy is consulted at all. Watch which set it lands in: a car call above the car goes into upRequests, below it into downRequests.
Run it and watch the sets drain
Press ▶ Run. Each tick moves every car one floor. Watch the chips: the car drains the set matching its direction in sort order, doors flash open on each stop, and the chip disappears. When nothing is left ahead, the direction flips and the other set becomes the live one.
The comparison — this is the whole lesson
Press ⚡ Rush hour (seven hall calls, always the same ones), make sure 🧭 LOOK is on, and press ▶ Run. Read floors travelled: 11. Now press 🔀 FIFO — the identical requests are replayed from the start — and run again: 21. Same building, same people, same cars. Nearly double the travel, from one line of ordering logic.
Swap the dispatcher without changing the caller
Press ⚖️ Least load, then make a few hall calls near one car. Under 🎯 Nearest car they all pile onto the closest car; under least-load they spread out. The call line is system.request(...) in both cases — it never changed. That is the strategy seam paying for itself.
Build it from memory
Blank file, in this order: Direction and CarState enums → HallCall(floor, direction) and CarCall(floor) → ElevatorCar with upRequests ascending and downRequests descending → step() with the flip-when-nothing-ahead rule → SchedulingStrategy with a nearest-car implementation → a main() that ticks in a loop and prints the total floors travelled. If your total for the stream 5, 2, 8, 3 from floor 1 is not 7, your serveHere() is not stopping for requests on the way.
In practice
When to use it — and what trips people up
The shape you just learned
Take the building away and this is a queue of requests that must be served in a smart order, and a fleet of workers to spread them across. Those are two independent decisions, and the whole design is about keeping them independent.
- Disk I/O scheduling — LOOK is literally the disk-arm algorithm. The head sweeps across cylinders serving requests on the way instead of seeking back and forth.
- Ride hailing — a rider request is a hall call with a direction; matching a driver is
SchedulingStrategy.pick(); picking up a second passenger on the way isserveHere(). - Delivery batching — a courier who already has your street on the route costs nothing extra; one heading the other way costs the detour penalty. Same cost function, different units.
- Print and job queues with a fleet of workers — dispatch to the least-loaded worker is
LeastLoadStrategywith a different name. - Any request router — load balancers make the same two decisions: which backend takes it, and in what order does that backend serve what it holds.
The two-sentence version to say out loud
“There are two request types — a hall call carries a direction, a car call carries a destination — and merging them makes correct scheduling impossible. Each car serves what it holds with LOOK: keep going, stop for everything on the way, reverse only when nothing is ahead; and which car takes a hall call is a separate strategy the caller never sees.” That is the design, in 20 seconds.
Where this design stops working
- At 60 floors and 8 cars. Per-car LOOK plus nearest-car dispatch causes bunching — cars drift into a clump and one region of the building waits. Real buildings switch to destination dispatch, where riders enter their destination in the lobby and the system groups them by trip, which gives the scheduler the information a hall call deliberately hides.
- When fairness matters more than efficiency. LOOK can starve a floor at the far end of a busy building. The fix is an age term in the cost — a request that has waited long enough overrides distance — and that is a real change to the algorithm, not a parameter.
- When cars share a shaft. Two cars in one shaft cannot pass each other, so scheduling becomes a constraint problem, not a sorting problem. Nothing you wrote survives that, and it is fine to say so.
- When you need optimality. Assigning N requests to M cars to minimise total travel is a hard combinatorial problem. Every real system uses a heuristic, which is exactly why the decision lives behind an interface.
If you only remember one thing
Model the direction, then never serve in arrival order. A HallCall(floor, direction) plus two sorted sets is maybe fifteen lines, and it is the difference between 7 floors and 18 on four requests. Everything else in this problem — doors, states, strategies, locks — is decoration on those two ideas.
What it gives you
- Separating HallCall from CarCall keeps the direction of every waiting rider, which is the one piece of information LOOK cannot work without.
- Two sorted sets turn scheduling into a data-structure choice: the ordering is free, and step() stays about fifteen lines.
- Putting dispatch behind SchedulingStrategy means a new policy is one new class and zero edits to ElevatorSystem.request().
- Driving time with tick() makes the whole scheduler testable synchronously — three calls and an assertion, no threads and no waiting.
- One lock per car around the two request sets is enough for correctness, and it never blocks the movement of any car.
Common mistakes
- Per-car LOOK plus nearest-car dispatch causes bunching in tall buildings — cars clump together and one region waits longer than it should.
- LOOK has no fairness guarantee: a request at the far end of a busy building can wait a long time unless you add an age term to the cost.
- The cost function is a heuristic, not an optimum — assigning many requests across many cars optimally is a hard combinatorial problem.
- A tick model quantises time into whole floors, so it cannot express acceleration, variable floor heights, or realistic door dwell times.
- Because dispatch reads car state without a lock, a request can occasionally be assigned using a floor number that is one tick stale.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
import java.util.*;
enum Direction { UP, DOWN, IDLE }
enum CarState { IDLE, MOVING_UP, MOVING_DOWN, DOORS_OPEN }
/**
* TWO kinds of request, and they are NOT the same thing.
* HallCall : pressed outside. Knows a floor AND a direction, but no destination.
* CarCall : pressed inside. Knows a destination, but carries no direction.
* Collapsing both into "a floor number" is what makes correct scheduling impossible.
*/
sealed interface Request permits HallCall, CarCall { int floor(); }
record HallCall(int floor, Direction direction) implements Request {}
record CarCall(int floor) implements Request {}
class ElevatorCar {
private final int id;
private int floor;
private Direction direction = Direction.IDLE;
private CarState state = CarState.IDLE;
private int floorsTravelled = 0;
// Two sorted sets. The whole algorithm is "drain the one matching my direction, then flip".
private final NavigableSet<Integer> upRequests = new TreeSet<>();
private final NavigableSet<Integer> downRequests = new TreeSet<>(Comparator.reverseOrder());
private final Object queueLock = new Object(); // guards the two sets, nothing else
ElevatorCar(int id, int startFloor) { this.id = id; this.floor = startFloor; }
int id() { return id; }
int floor() { return floor; }
Direction direction() { return direction; }
CarState state() { return state; }
int floorsTravelled() { return floorsTravelled; }
int pending() { synchronized (queueLock) { return upRequests.size() + downRequests.size(); } }
boolean busy() { return pending() > 0 || state == CarState.DOORS_OPEN; }
/** Hall calls arrive from many floors at once — this is the only shared state. */
void addRequest(Request r) {
synchronized (queueLock) {
if (r instanceof HallCall h) {
if (h.direction() == Direction.UP) upRequests.add(h.floor());
else downRequests.add(h.floor());
} else { // a CarCall: direction comes from where we are
if (r.floor() > floor) upRequests.add(r.floor());
else if (r.floor() < floor) downRequests.add(r.floor());
else state = CarState.DOORS_OPEN; // already standing here
}
}
}
/** ONE floor of movement. Time is driven from outside — never sleep in here. */
void step() {
if (state == CarState.DOORS_OPEN) { state = resume(); return; } // doors take one tick
if (!busy()) { direction = Direction.IDLE; state = CarState.IDLE; return; }
if (direction == Direction.IDLE) direction = towardsNearest();
if (serveHere()) { state = CarState.DOORS_OPEN; return; }
if (!workAhead(direction)) direction = opposite(direction); // nothing left this way
if (serveHere()) { state = CarState.DOORS_OPEN; return; }
floor += (direction == Direction.UP) ? 1 : -1;
floorsTravelled++;
state = (direction == Direction.UP) ? CarState.MOVING_UP : CarState.MOVING_DOWN;
if (serveHere()) state = CarState.DOORS_OPEN; // stop for it on the way
}
/** Remove a request at this floor IF it matches where we are going. */
private boolean serveHere() {
synchronized (queueLock) {
if (direction == Direction.UP) {
if (upRequests.remove(floor)) return true;
if (!anyAbove() && downRequests.remove(floor)) return true; // turnaround point
} else {
if (downRequests.remove(floor)) return true;
if (!anyBelow() && upRequests.remove(floor)) return true;
}
return false;
}
}
private CarState resume() {
if (pending() == 0) { direction = Direction.IDLE; return CarState.IDLE; }
return direction == Direction.UP ? CarState.MOVING_UP : CarState.MOVING_DOWN;
}
private Direction towardsNearest() {
int best = Integer.MAX_VALUE, target = floor;
for (int f : allRequests())
if (Math.abs(f - floor) < best) { best = Math.abs(f - floor); target = f; }
return target >= floor ? Direction.UP : Direction.DOWN;
}
private boolean workAhead(Direction d) { return d == Direction.UP ? anyAbove() : anyBelow(); }
private boolean anyAbove() { for (int f : allRequests()) if (f > floor) return true; return false; }
private boolean anyBelow() { for (int f : allRequests()) if (f < floor) return true; return false; }
private List<Integer> allRequests() {
synchronized (queueLock) {
List<Integer> out = new ArrayList<>(upRequests);
out.addAll(downRequests);
return out;
}
}
private static Direction opposite(Direction d) { return d == Direction.UP ? Direction.DOWN : Direction.UP; }
String describe() {
synchronized (queueLock) {
return "car#" + id + " floor " + floor + " " + pad(state.name()) + " up" + upRequests + " down" + downRequests;
}
}
private static String pad(String s) { return (s + " ").substring(0, 11); }
}
/** WHICH car takes a hall call. The one decision the interviewer will ask you to change. */
interface SchedulingStrategy {
ElevatorCar pick(List<ElevatorCar> cars, HallCall call);
}
class NearestCarStrategy implements SchedulingStrategy {
private static final int DETOUR = 100; // big enough that a wrong-way car only wins if nothing else is close
public ElevatorCar pick(List<ElevatorCar> cars, HallCall call) {
ElevatorCar best = null;
int bestCost = Integer.MAX_VALUE;
for (ElevatorCar c : cars) {
int cost = cost(c, call);
if (cost < bestCost) { bestCost = cost; best = c; }
}
return best;
}
static int cost(ElevatorCar c, HallCall call) {
int distance = Math.abs(c.floor() - call.floor());
if (c.direction() == Direction.IDLE) return distance; // idle: just the walk
boolean towards = (c.direction() == Direction.UP && call.floor() >= c.floor())
|| (c.direction() == Direction.DOWN && call.floor() <= c.floor());
if (towards && c.direction() == call.direction()) return distance; // it passes you anyway
return distance + DETOUR; // it must finish its run first
}
}
class LeastLoadStrategy implements SchedulingStrategy {
public ElevatorCar pick(List<ElevatorCar> cars, HallCall call) {
ElevatorCar best = null;
int bestCost = Integer.MAX_VALUE;
for (ElevatorCar c : cars) {
int cost = c.pending() * 100 + Math.abs(c.floor() - call.floor());
if (cost < bestCost) { bestCost = cost; best = c; }
}
return best;
}
}
class ElevatorSystem {
private final List<ElevatorCar> cars;
private SchedulingStrategy strategy;
ElevatorSystem(List<ElevatorCar> cars, SchedulingStrategy strategy) {
this.cars = List.copyOf(cars);
this.strategy = strategy;
}
/** Open for a new policy, closed for modification — request() below never changes. */
void setStrategy(SchedulingStrategy s) { this.strategy = s; }
ElevatorCar request(HallCall call) {
ElevatorCar car = strategy.pick(cars, call); // WHICH car — pluggable
car.addRequest(call); // WHEN to serve it — the car's own LOOK
return car;
}
ElevatorCar car(int id) {
for (ElevatorCar c : cars) if (c.id() == id) return c;
throw new NoSuchElementException("no car " + id);
}
/** Time comes from OUTSIDE: a main loop, a test, or a hardware timer. */
void tick() { for (ElevatorCar c : cars) c.step(); }
boolean busy() { for (ElevatorCar c : cars) if (c.busy()) return true; return false; }
int floorsTravelled() { int t = 0; for (ElevatorCar c : cars) t += c.floorsTravelled(); return t; }
List<ElevatorCar> cars() { return cars; }
}
public class Main {
/** What FIFO would have cost: walk to each request in arrival order. */
static int fifoFloors(int start, List<Integer> arrivalOrder) {
int total = 0, at = start;
for (int f : arrivalOrder) { total += Math.abs(f - at); at = f; }
return total;
}
public static void main(String[] args) {
System.out.println("=== one car, four hall calls: 5 up, 2 up, 8 up, 3 up ===");
ElevatorSystem one = new ElevatorSystem(List.of(new ElevatorCar(1, 1)), new NearestCarStrategy());
for (int f : new int[] { 5, 2, 8, 3 }) one.request(new HallCall(f, Direction.UP));
int t = 0;
while (one.busy() && t < 200) { // the caller owns time
one.tick();
t++;
System.out.println("t=" + (t < 10 ? "0" : "") + t + " " + one.car(1).describe());
}
System.out.println("LOOK floors travelled: " + one.floorsTravelled());
System.out.println("FIFO floors travelled: " + fifoFloors(1, List.of(5, 2, 8, 3)) + " (1 -> 5 -> 2 -> 8 -> 3)");
System.out.println();
System.out.println("=== two cars, dispatch is a separate decision ===");
ElevatorSystem bank = new ElevatorSystem(
List.of(new ElevatorCar(1, 1), new ElevatorCar(2, 9)), new NearestCarStrategy());
for (int f : new int[] { 8, 7, 6 })
System.out.println("hall call " + f + " DOWN -> car #" + bank.request(new HallCall(f, Direction.DOWN)).id());
bank.setStrategy(new LeastLoadStrategy()); // the ONLY line that changes
System.out.println("swapped strategy to LeastLoad — request() did not change");
System.out.println("hall call 5 DOWN -> car #" + bank.request(new HallCall(5, Direction.DOWN)).id());
}
}
/* expected output
=== one car, four hall calls: 5 up, 2 up, 8 up, 3 up ===
t=01 car#1 floor 2 DOORS_OPEN up[3, 5, 8] down[]
t=02 car#1 floor 2 MOVING_UP up[3, 5, 8] down[]
t=03 car#1 floor 3 DOORS_OPEN up[5, 8] down[]
t=04 car#1 floor 3 MOVING_UP up[5, 8] down[]
t=05 car#1 floor 4 MOVING_UP up[5, 8] down[]
t=06 car#1 floor 5 DOORS_OPEN up[8] down[]
t=07 car#1 floor 5 MOVING_UP up[8] down[]
t=08 car#1 floor 6 MOVING_UP up[8] down[]
t=09 car#1 floor 7 MOVING_UP up[8] down[]
t=10 car#1 floor 8 DOORS_OPEN up[] down[]
t=11 car#1 floor 8 IDLE up[] down[]
LOOK floors travelled: 7
FIFO floors travelled: 18 (1 -> 5 -> 2 -> 8 -> 3)
=== two cars, dispatch is a separate decision ===
hall call 8 DOWN -> car #2
hall call 7 DOWN -> car #2
hall call 6 DOWN -> car #2
swapped strategy to LeastLoad — request() did not change
hall call 5 DOWN -> car #1
*/References & further reading
7 sources- Articlegithub.com
awesome-low-level-design — Elevator system problem
The problem written up in the format interviewers use, with a reference solution to compare your class list against.
- Articleen.wikipedia.org
Disk scheduling: SCAN, C-SCAN and LOOK
The algorithm is named after this problem and is still taught as the disk-arm scheduler. Short, and it names the variants you can offer as follow-ups.
- Book
Operating System Concepts — Silberschatz, Galvin & Gagne
The disk-scheduling chapter compares FCFS, SSTF, SCAN, C-SCAN and LOOK with worked head-movement totals — the same arithmetic as the 18-versus-7 figure.
- Docsdocs.oracle.com
TreeSet — Java API documentation
Why a TreeSet and a TreeSet(reverseOrder()) are the right containers: sorted iteration, no duplicates, and O(log n) add and remove.
- Articleen.wikipedia.org
Destination dispatch — how modern lift banks actually work
Read this before the interview. It is the answer to “what changes at 60 floors?”, and it explains why hall calls disappear entirely.
- Paperpeters-research.com
Elevator traffic analysis and handling capacity
Real lift-engineering papers on round-trip time and handling capacity — useful if you want to defend a cost function with numbers rather than intuition.
- Docsrefactoring.guru
Refactoring Guru — Strategy pattern
The seam used for SchedulingStrategy, with the same before-and-after shape: a family of interchangeable algorithms behind one call.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Sign in to save your best score.
question 01 / 08
Why must a hall call and a car call be different types?
question 02 / 08
A car sits at floor 1. Up calls arrive for floors 5, 2, 8 and 3, in that order. How many floors does the car travel under FIFO, and under LOOK?
question 03 / 08
Your car is at floor 5 moving up, with a request at floor 9. Someone on floor 7 presses the DOWN button. What should the car do?
question 04 / 08
Why keep two sorted sets per car instead of one sorted list of pending floors?
question 05 / 08
Where should the “which car takes this hall call” decision live?
question 06 / 08
Why should movement be driven by a tick() call instead of a sleep inside the car?
question 07 / 08
A firefighter mode is requested. What is the part candidates most often forget?
question 08 / 08
The building grows to 60 floors and 8 cars. What is the real structural change?
0/8 answered