Elevator System
Design a system that receives elevator requests, chooses a car, and moves each car one floor at a time until every request is served.

1. Requirements (~5 minutes)
The prompt is intentionally short:
“Design an elevator system for a building.”
The word elevator may immediately make you think of buttons, doors, displays, motors, and sensors. That is useful real-world knowledge, but it is too much to design at once. Before choosing classes, we need to discover which small part of the real system the interviewer wants us to model.
Think of the requirements as a promise: what must our finished code be able to demonstrate? Every later class and method should help keep that promise.
Use these four question groups every time
What must a user be able to do?
When does an action succeed, fail, or change state?
Which invalid actions must we reject?
What should we deliberately not build?
Ask these questions
AskHow many floors and elevators should we support? Should these values be fixed?Show answerHide answer
Interviewer answer
Use three elevators and floors 0 through 9. They can be fixed for now.
AskWhat can a passenger do outside and inside an elevator?Show answerHide answer
Interviewer answer
Outside, a passenger requests UP or DOWN. Inside, a passenger selects a destination floor.
AskWhen should a moving elevator stop for a hall call?Show answerHide answer
Interviewer answer
Serve a hall call only when the car is moving in the requested direction. Destinations are always served.
AskHow does time move in this design? Are we controlling real hardware?Show answerHide answer
Interviewer answer
Build a simulation. One tick moves each active elevator by at most one floor.
AskWhat should happen for an invalid floor, a duplicate request, or the current floor?Show answerHide answer
Interviewer answer
Reject invalid floors, ignore duplicates, and treat the current floor as already served.
AskShould we include doors, weight limits, emergencies, or real concurrency?Show answerHide answer
Interviewer answer
No. Discuss them only as later extensions.
Confirm the specification
Confirmed specification
7 requirements · 5 boundaries
The first version must
- 1.Manage 3 elevators serving floors 0 through 9.
- 2.Accept UP or DOWN hall calls and assign one elevator.
- 3.Accept one or more destination floors inside each elevator.
- 4.Advance the simulation one tick at a time.
- 5.Serve a hall call only while moving in its requested direction.
- 6.Reject out-of-range floors without changing state.
- 7.Ignore duplicate requests and treat the current floor as already served.
Not building in this version5 boundaries
- Doors and physical sensors
- Weight and passenger capacity
- Emergency and maintenance modes
- Persistence or networking
- Real concurrent threads
2. Entities and relationships (~3 minutes)
We have agreed on the behavior. Now we can ask: which objects are needed to make that behavior happen?
Start by scanning the confirmed requirements for important nouns: elevator, floor, request, direction, and system. These are only candidates. A noun does not automatically deserve a class.
For each candidate, ask two simple questions:
- Does it remember information that changes while the program runs?
- Does it protect or perform an important rule?
If the answer to both is no, it is probably a field, an enum, or something we can leave out.
Final model
Types we will create
- Class
- Owns changing state or coordinates rules.
- Record
- A Java record for validated, immutable data with value equality.
- Interface
- Defines behavior that another implementation can replace.
Owns one car's movement state and pending requests.
Coordinates hall calls and the fleet.
Keeps a floor request and its validated type together.
Keeps the fleet-selection rule replaceable.
Enums
Enums name a small, fixed set of valid choices or outcomes.
Names whether a car moves UP, DOWN, or is IDLE.
Distinguishes hall pickups from inside destinations.
Fields
Fields are simple values stored inside another type; they do not need independent behavior in this version.
Identifies a requested or current building level.
See the objects working together
Swipe to follow the flow →

Read the arrows as a short story. A passenger creates a hall call. The controller compares the elevators and selects one car. That car stores the request. Before the next selection, the controller can read each car's floor and direction. This explains why the controller needs access to the fleet but should not own a car's movement logic.
3. Class design (10–15 minutes)
We now have the objects, but not their fields or methods. Do not fill the classes from memory. Go back to each requirement and ask, what must this object remember, and what must another object ask it to do?
This creates a traceable chain:
requirement → responsible object → state it remembers → method that changes or reads that state
One class at a time
Design the state owners
Start with the coordinator, then follow the request into the classes that protect changing state. Select a class to see only the fields and behavior needed for its responsibility.
Class 1 of 2
ElevatorController
Coordinates hall calls and advances the fleet without calculating a car's movement itself.
Requirement behind this class
A passenger can request a car, a replaceable rule selects one elevator, and the system advances predictably.
State it remembers
Private information required to perform this class's job.
Validates every hall call and destination.
Stores the fleet compared for hall calls.
Delegates the replaceable car-selection rule.
Behavior it owns
Public use cases and internal rules kept beside that state.
Creates a hall Request, selects a car, and adds the stop.
Adds a validated inside-car destination to one known Elevator.
Asks every Elevator to advance one deterministic step.
Exposes an unmodifiable fleet view for display and tests.
Keep lookup and building-bound checks inside the controller.
- Invariant it protects
- Every accepted hall call is assigned through the configured Strategy to a known Elevator.
- Works with
- Elevator, Request, and DispatchStrategy.
- Does not own
- A car's current floor, direction, stop ordering, or movement rules.
- Principle demonstrated
- Strategy and composition separate fleet coordination from car movement and selection policy.
Supporting types
These types carry stable data or define replaceable behavior. They support the state owners without becoming another place to store live workflow state.
Carries a validated floor and request type.
Generated equality lets the pending set remove duplicate requests naturally.
Defines selection from the fleet for one hall call.
The controller depends on the contract while Elevator remains unchanged when dispatch policy varies.
Prefers nearby cars already moving toward the caller.
It only selects; it does not mutate a car or store fleet state.
Decide whether a pattern helps
Only now should we discuss patterns. Look for a rule that may have more than one valid version. In this problem, car selection may change while the meaning of an elevator remains the same.
Use: Strategy for elevator selection
The rule for choosing an elevator can change independently from movement. We may start with “nearest car” and later use “direction-aware” or “lowest estimated wait.” A DispatchStrategy interface lets the controller use any of those rules without changing Elevator.
Do not force: Singleton for the controller
Nothing in the requirements says only one controller object may exist in the entire program. A singleton would add global state and make tests harder. Create a normal controller object instead.
Do not force: Factory for requests
Creating a request needs only a floor and type. A constructor is already clear. A factory would add another class without solving a problem.
4. Implementation (~10 minutes)
Below is the complete Java version. Each file has one job. The code uses Java 17 or later.
What the code does
It accepts hall calls and destination requests, chooses an elevator, and advances every car by one predictable tick. Each car decides whether to stop, continue, reverse, or become idle.
Why it is structured this way
The controller owns work that concerns the whole building. Each Elevator owns only its own position, direction, and pending stops. The selection rule is behind an interface because interviewers often ask to replace that rule.
Follow one request through the code
- 1ElevatorController.requestElevator receives a floor and direction.
- 2It creates a typed Request and asks DispatchStrategy to select one car.
- 3The selected Elevator stores the request after validating the floor.
- 4Each tick lets every Elevator serve, move, reverse, or become idle.
Code concepts to understand
Open these explanations before reading the related files. They explain the decision, not just the definition.
Strategy pattern: choosing an elevatorThe controller delegates the replaceable car-selection rule to DispatchStrategy.
The changing question is: which elevator should receive this hall call? ElevatorController should not contain every possible answer to that question.
DispatchStrategydefines one operation:select(elevators, hallCall).DirectionAwareDispatchStrategysupplies the current rule.- The controller uses the interface, so it does not know how the score is calculated.
- A nearest-car or least-busy strategy can be added without changing
Elevatoror the request workflow.
This pattern is useful here because the selection rule is likely to change. It is not used for movement because this version has only one movement rule.
Immutable request valueRequest records what was asked without owning system behavior.
Request has final fields and no setters. Once created, its floor and type cannot change. That makes queued requests safe to compare and easy to reason about.
equals and hashCode use the same fields because requests are stored in a Set. Two requests with the same floor and type are treated as duplicates.
Encapsulation: each object protects its own stateThe controller coordinates cars, while an Elevator alone changes its floor, direction, and queue.
The controller calls addRequest and tick; it does not directly edit an elevator's fields. getRequests returns an unmodifiable view, so outside code can inspect the queue but cannot bypass validation.
This keeps one clear place for movement rules and prevents the controller from becoming a class that does everything.
Deterministic ticks instead of real timersOne tick means one small, testable unit of time.
A real elevator is concurrent and event-driven, but threads and wall-clock timers would hide the design being discussed. tick() makes every state change repeatable: a test can call it once and know exactly what should happen.
Real concurrency can be added later around the same domain rules.
Enums and request record
Start with the smallest vocabulary. Direction describes motion, RequestType explains why a car should stop, and Request combines a floor with that reason. Keeping these meanings explicit prevents unclear booleans such as isPickup or direction strings such as "up".
public enum Direction {
UP,
DOWN,
IDLE;
public Direction opposite() {
return switch (this) {
case UP -> DOWN;
case DOWN -> UP;
case IDLE -> IDLE;
};
}
}public enum RequestType {
PICKUP_UP,
PICKUP_DOWN,
DESTINATION;
public boolean matches(Direction direction) {
return this == DESTINATION
|| (this == PICKUP_UP && direction == Direction.UP)
|| (this == PICKUP_DOWN && direction == Direction.DOWN);
}
}import java.util.Objects;
public record Request(int floor, RequestType type) {
public Request {
type = Objects.requireNonNull(type);
}
@Override
public String toString() {
return "Request{floor=" + floor + ", type=" + type + "}";
}
}Elevator entity
Elevator is the main state-owning object. Read its methods in this order: the constructor establishes valid limits, addRequest protects the queue, and tick performs one state change. The private helpers answer smaller questions so the main workflow remains readable.
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
public final class Elevator {
private final int id;
private final int minFloor;
private final int maxFloor;
private int currentFloor;
private Direction direction;
private final Set<Request> requests = new LinkedHashSet<>();
public Elevator(int id, int minFloor, int maxFloor, int startFloor) {
if (minFloor > maxFloor) {
throw new IllegalArgumentException("minFloor must be <= maxFloor");
}
if (startFloor < minFloor || startFloor > maxFloor) {
throw new IllegalArgumentException("startFloor is out of range");
}
this.id = id;
this.minFloor = minFloor;
this.maxFloor = maxFloor;
this.currentFloor = startFloor;
this.direction = Direction.IDLE;
}
public boolean addRequest(Request request) {
if (request == null || !isValidFloor(request.floor())) {
return false;
}
if (request.floor() == currentFloor) {
return true; // already served
}
return requests.add(request); // false for a duplicate
}
public void tick() {
if (requests.isEmpty()) {
direction = Direction.IDLE;
return;
}
if (direction == Direction.IDLE) {
direction = directionToNearestRequest();
}
if (serveCurrentFloor()) {
if (requests.isEmpty()) direction = Direction.IDLE;
return; // stopping consumes this tick
}
if (!hasRequestAhead(direction)) {
direction = direction.opposite();
return; // reversing consumes this tick
}
currentFloor += direction == Direction.UP ? 1 : -1;
}
private boolean serveCurrentFloor() {
return requests.removeIf(request ->
request.floor() == currentFloor
&& request.type().matches(direction));
}
private boolean hasRequestAhead(Direction travelDirection) {
return requests.stream().anyMatch(request ->
travelDirection == Direction.UP
? request.floor() > currentFloor
: request.floor() < currentFloor);
}
private Direction directionToNearestRequest() {
Request nearest = requests.stream()
.min((left, right) -> {
int leftDistance = Math.abs(left.floor() - currentFloor);
int rightDistance = Math.abs(right.floor() - currentFloor);
int byDistance = Integer.compare(leftDistance, rightDistance);
return byDistance != 0
? byDistance
: Integer.compare(left.floor(), right.floor());
})
.orElseThrow();
return nearest.floor() > currentFloor
? Direction.UP
: Direction.DOWN;
}
private boolean isValidFloor(int floor) {
return floor >= minFloor && floor <= maxFloor;
}
public int getId() {
return id;
}
public int getCurrentFloor() {
return currentFloor;
}
public Direction getDirection() {
return direction;
}
public Set<Request> getRequests() {
return Collections.unmodifiableSet(requests);
}
}Selection strategy
These two files form one replaceable seam. The interface tells the controller what it may ask for. The implementation ranks cars by direction, distance, and finally ID so equal choices still produce a predictable answer.
import java.util.List;
public interface DispatchStrategy {
Elevator select(List<Elevator> elevators, Request hallCall);
}import java.util.Comparator;
import java.util.List;
public final class DirectionAwareDispatchStrategy
implements DispatchStrategy {
@Override
public Elevator select(List<Elevator> elevators, Request hallCall) {
if (elevators == null || elevators.isEmpty()) {
throw new IllegalArgumentException("At least one elevator is required");
}
if (hallCall == null || hallCall.type() == RequestType.DESTINATION) {
throw new IllegalArgumentException("A hall call is required");
}
Direction requestedDirection = hallCall.type() == RequestType.PICKUP_UP
? Direction.UP
: Direction.DOWN;
return elevators.stream()
.min(Comparator
.comparingInt((Elevator elevator) -> priority(
elevator, hallCall.floor(), requestedDirection))
.thenComparingInt(elevator -> Math.abs(
elevator.getCurrentFloor() - hallCall.floor()))
.thenComparingInt(Elevator::getId))
.orElseThrow();
}
private int priority(
Elevator elevator,
int requestedFloor,
Direction requestedDirection) {
boolean movingTowardCaller =
elevator.getDirection() == requestedDirection
&& (requestedDirection == Direction.UP
? elevator.getCurrentFloor() <= requestedFloor
: elevator.getCurrentFloor() >= requestedFloor);
if (movingTowardCaller) return 0;
if (elevator.getDirection() == Direction.IDLE) return 1;
return 2;
}
}Controller and example program
ElevatorController is the entry point for building-level actions. It validates public input, translates it into domain objects, delegates selection, and advances the fleet. ElevatorDemo is intentionally small: it shows the order in which a caller creates the system, submits a request, and advances time.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public final class ElevatorController {
private final int minFloor;
private final int maxFloor;
private final List<Elevator> elevators;
private final DispatchStrategy dispatchStrategy;
public ElevatorController(
int elevatorCount,
int minFloor,
int maxFloor,
DispatchStrategy dispatchStrategy) {
if (elevatorCount <= 0) {
throw new IllegalArgumentException("elevatorCount must be positive");
}
if (minFloor > maxFloor) {
throw new IllegalArgumentException("minFloor must be <= maxFloor");
}
this.minFloor = minFloor;
this.maxFloor = maxFloor;
this.dispatchStrategy = Objects.requireNonNull(dispatchStrategy);
this.elevators = new ArrayList<>();
for (int id = 1; id <= elevatorCount; id++) {
elevators.add(new Elevator(id, minFloor, maxFloor, minFloor));
}
}
public boolean requestElevator(int floor, Direction direction) {
if (!isValidFloor(floor) || direction == null || direction == Direction.IDLE) {
return false;
}
RequestType type = direction == Direction.UP
? RequestType.PICKUP_UP
: RequestType.PICKUP_DOWN;
Request hallCall = new Request(floor, type);
Elevator selected = dispatchStrategy.select(elevators, hallCall);
return selected.addRequest(hallCall);
}
public boolean selectDestination(int elevatorId, int floor) {
Elevator elevator = findElevator(elevatorId);
if (elevator == null || !isValidFloor(floor)) return false;
return elevator.addRequest(new Request(floor, RequestType.DESTINATION));
}
public void tick() {
elevators.forEach(Elevator::tick);
}
private Elevator findElevator(int elevatorId) {
return elevators.stream()
.filter(elevator -> elevator.getId() == elevatorId)
.findFirst()
.orElse(null);
}
private boolean isValidFloor(int floor) {
return floor >= minFloor && floor <= maxFloor;
}
public List<Elevator> getElevators() {
return Collections.unmodifiableList(elevators);
}
}public final class ElevatorDemo {
public static void main(String[] args) {
ElevatorController controller = new ElevatorController(
3,
0,
9,
new DirectionAwareDispatchStrategy());
controller.requestElevator(5, Direction.UP);
for (int tick = 1; tick <= 8; tick++) {
controller.tick();
System.out.println("Tick " + tick);
controller.getElevators().forEach(elevator ->
System.out.printf(
"Car %d: floor=%d, direction=%s, requests=%s%n",
elevator.getId(),
elevator.getCurrentFloor(),
elevator.getDirection(),
elevator.getRequests()));
}
}
}Verify the design with a real scenario
- 1Initial: all cars are idle at floor 0 and all request sets are empty.
- 2requestElevator(5, UP) creates Request(5, PICKUP_UP).
- 3The strategy chooses car 1 because every car has the same priority and car 1 wins the stable ID tie-break.
- 4Ticks 1–5 move car 1 from floor 0 to floor 5. Other cars stay idle.
- 5Tick 6 finds a matching UP pickup at floor 5, removes it, and makes the car IDLE.
- 6requestElevator(12, UP) returns false. No elevator state changes.
Use the simulation to try the same sequence. Then load the edge case and compare the two selection rules.
Try it yourself
Elevator simulator
How should we choose a car?
Create hall call
Event log
- Simulation ready. Add a hall call or destination.
5. Extensions (~5 minutes)
“Use a different assignment rule”
Add another DispatchStrategy implementation. The controller and elevator classes do not change. This is the exact change the Strategy pattern was introduced to support.
“Add express elevators”
Give each elevator a set of supported floors and reject unsupported destinations in addRequest. The controller must filter out cars that cannot serve the requested floor before calling the strategy.
“Requests can arrive from different threads”
The selection and assignment must happen as one protected operation; otherwise two callers can both observe the same idle car. Protect requestElevator, or place incoming hall calls in a thread-safe queue that one controller loop consumes.
“Add doors and weight limits”
This changes the state machine. Add door state and capacity only now that the requirement exists. A car should move only when doors are closed, and should reject boarding when capacity is reached.