Elevator
Understanding the Problem
An elevator system manages multiple elevators serving different floors in a building. When someone requests an elevator, the system decides which one to dispatch. Once inside, passengers select their destination floors. The system needs to move elevators efficiently while handling multiple concurrent requests.
Requirements
As the interview begins, we'll likely be greeted by a simple prompt to set the stage for the architecture we need to design.
Before jumping into class design, we should ask questions of our interviewer. The goal here is to turn that vague prompt into a concrete specification, something we can actually build against.
The goal is to surface ambiguity early and get to a concrete spec. This mirrors the same process we use when clarifying requirements on a real project.
A reliable way to structure our questions is to cover four areas: what the core actions are, how errors should be handled, what the boundaries of the system are, and whether we need to plan for future extensions.
Here's how a conversation between us and the interviewer might go. In a real interview, this back-and-forth typically takes 3 to 5 minutes. Don't rush it. Getting clear requirements upfront saves time later.
Good. We've established the scale. This matters because the complexity changes dramatically with scale. Three elevators is very different from 300.
This tells us we're modeling the usual two-button hall calls (not destination dispatch), and dispatch logic is in scope with flexibility in how sophisticated it needs to be.
Now we know elevators need to handle multiple destination requests, not just one at a time.
This is critical. The interviewer just told us the core movement behavior. This is much closer to how real elevator algorithms behave. It's more efficient than serving requests in the order they arrived (FIFO) because it minimizes direction changes.
We've identified a tradeoff and scoped it appropriately. The interviewer is telling us to keep dispatch simple initially.
This question saved us from spending 15 minutes designing door state machines and weight sensors. In interviews, knowing what to explicitly exclude is just as important as knowing what to include. You only have 35 minutes.
That question about simulation vs. control software might seem overly specific, but it's one of the most important clarifications we can make in this type of interview.
A real elevator system doesn't have a step() function we call to advance time. It has motor controllers that physically move the car, floor sensors that fire interrupts on arrival, and asynchronous control flow driven by hardware events. You'd see callbacks like onFloorReached(floor) instead of manually incrementing currentFloor++ .
But in LLD interviews, we're almost always expected to build a simulation. Whether the problem involves elevators, parking lots, vending machines, or traffic lights, the pattern is the same. Abstract away the hardware and control time yourself with step() or tick() . This keeps the problem tractable in 45 minutes, makes the logic deterministic and testable, and lets us focus on object modeling and algorithms rather than concurrency and hardware interfaces.
When you encounter a problem like this, ask explicitly, "Are we building a simulation where I control time, or modeling actual control software?" This signals you understand the distinction (a senior-level insight) and prevents you from going down the wrong path. Nine times out of ten, they want the simulation, and you can confidently focus on your object model and scheduling algorithm rather than motor controllers and sensor interrupts.
Final Requirements
After that back-and-forth, we can write down the final requirements, as well as any learning about what is out of scope, on the whiteboard.
Requirements:
1. System manages 3 elevators serving 10 floors (0-9)
2. Users can request an elevator from any floor (hall call). System decides which elevator to dispatch.
3. Once inside, users can select one or more destination floors
4. Simulation runs in discrete time steps (e.g., a `step()` or `tick()` call advances time)
5. Elevator movement behavior
- Continue in current direction servicing all requests
- Reverse direction when no more stops ahead
6. System handles multiple concurrent pickup requests across floors
7. Invalid requests should be rejected
- Non-existent floor numbers
- For simplicity, treat requests to add the current floor as invalid
Out of scope:
- Weight capacity and passenger limits
- Door open/close mechanics
- Emergency stop functionality
- Dynamic floor/elevator configuration
- UI/rendering layer
Core Entities and Relationships
Now that we've locked down the requirements, we need to identify the core objects that will make up our system.
The trick is to scan through our requirements and spot the key entities. These are usually nouns that seem to have behavior or state. For an elevator system, a few obvious candidates jump out: elevators, floors, requests, and some kind of central coordinator that dispatches work.
Let's think through each potential entity before narrowing it down to the final list.
Floor - At first glance, floors seem important. But what would a Floor class actually do? In our requirements, floors don't maintain state or enforce rules. They're just numbers that identify positions in the building. A floor doesn't "do" anything. This stays as an integer, not a class.
Request - When someone presses a button, that's a request. Should we model this as a class? What would it contain? The floor number, maybe a direction (up/down), maybe a timestamp. But here's the thing. Requests don't maintain state or enforce rules. They're just data that gets passed around and stored. We could make a Request class "for future extensibility," but that violates YAGNI (You Aren't Gonna Need It). Keep it simple. Store requests as primitives. If requirements change later and we need request IDs, priorities, or timestamps, we can promote it to a class then.
When we're unsure if something should be a class, ask yourself two questions: "What methods would this class have?" and "Does this thing maintain changing state or enforce rules?" If the only methods are getters and setters with no real behavior, and it doesn't track changing state or enforce constraints, it's probably just data. Keep it as a primitive or a field on another class. Not every noun deserves to be an entity.
Elevator - Elevators definitely maintain state (current floor, direction, which floors to stop at) and enforce rules (must service stops along their path, can't go below floor 0). Clear candidate for an entity.
ElevatorController - Someone needs to receive hall calls and decide which elevator to dispatch. This is the orchestrator. It owns the system-level view of all elevators and makes coordination decisions. Another clear entity.
Whenever we're building a tick-based simulation, we'll need a controller entity that owns the step() function. This controller advances time for the entire system and orchestrates all the actors.
For this system, we can settle on just two core entities.
ElevatorController - The orchestrator. Receives hall calls from people on floors, decides which elevator should handle each request, and coordinates the overall system. Doesn't need to know the internals of how elevators move. It just dispatches requests and tells elevators to advance.
Elevator - Represents one elevator in the building. Maintains its current floor, direction, and queue of stops. Knows how to execute the movement behavior. Move one floor at a time, stop when needed, reverse when there are no more stops ahead. Doesn't know about other elevators.
Class Design
With our two core entities locked in, it's time to move on to flesh out what each one actually looks like. What state it holds and what operations it supports.
The smart move here is to work from the top down. Start with the ElevatorController since it's the entry point. External code interacts with the controller, not individual elevators. Design its interface first, then drill into the Elevator class. This approach keeps us thinking about the public contract instead of getting bogged down in implementation.
For both classes, we'll trace back to the requirements and ask what does this entity need to remember, and what actions does it need to support? Let's tackle ElevatorController first.
ElevatorController
We can derive almost everything about ElevatorController directly from the final set of requirements. During the interview, revisit the requirements and ask "What does the controller need to remember to enforce this?" This is how we'll derive the state.
This leads to the following state:
class ElevatorController:
- elevators: List<Elevator>
Wait, that's it? Where's the queue of pending hall calls? Where's the mapping of which elevator is assigned to which request?
In our design, hall calls are immediately dispatched to an elevator when they arrive. The controller doesn't maintain a queue of unassigned requests. It picks an elevator right away and tells that elevator to add the floor to its stops. This keeps the controller stateless beyond just holding the elevators.
We could design this differently. Maintain a queue of pending requests on the controller and have elevators pull from it. Both approaches work. The immediate dispatch model is simpler for an interview, but if the interviewer asks "what if all elevators are busy?", you'd want the queue model. Always be ready to explain your tradeoffs.
Next, look at the actions the outside world needs to perform. Again, every method on ElevatorController should correspond to a concrete need in the problem statement.
Adding the methods to the ElevatorController class, we get:
class ElevatorController:
- elevators: List<Elevator>
+ new ElevatorController() -> ElevatorController
+ requestElevator(floor: int, direction: Direction) -> void
+ step() -> void
The constructor initializes the three elevators:
ElevatorController() {
this.elevators = [
new Elevator(),
new Elevator(),
new Elevator()
]
}
requestElevator is where dispatch logic lives. When someone on floor 5 presses the "up" button, this method decides which of the 3 elevators should respond. We'll implement this later, but the signature captures the intent. Give me a floor and a direction, and I'll dispatch an elevator.
step is how time advances in our simulation. Each call represents one unit of time passing. The controller tells each elevator to take one step. Move one floor, handle stops, or update direction.
Elevator
Elevator represents one elevator in the building. From the requirements, we need to track position, movement direction, and which floors to visit.
We can derive its state directly from the problem:
This leads to:
class Elevator:
- currentFloor: int
- direction: Direction // UP, DOWN, IDLE
- stops: Set<int>
A couple of important choices here.
Why direction needs an IDLE state. When an elevator has no stops, it's not moving up or down. It's idle. We need an explicit state to represent "not moving" so the elevator doesn't keep drifting up or down forever. Some candidates try to get away with just UP and DOWN, then realize they can't handle the "no requests" case cleanly.
Why stops is a Set. If two people inside the elevator both press the button for floor 7, we only need to stop there once. A Set automatically handles deduplication. We could use a List, but then you'd need to check for duplicates yourself.
Here's a tradeoff we're making. Our Set just stores floor numbers, not direction. This means if someone on floor 5 presses "up" and someone else on floor 5 presses "down", we're treating those as the same stop. A more sophisticated design would store Set<(floor, direction)> and only stop if the direction matches. For interview scope, we're keeping it simple and we'll go into this option when we discuss extensions later in this breakdown. Always call out your simplifications explicitly.
From the outside, Elevator needs to support these actions:
That gives us this interface:
class Elevator:
- currentFloor: int
- direction: Direction // UP, DOWN, IDLE
- stops: Set<int>
+ new Elevator() -> Elevator
+ addStop(floor: int) -> void
+ step() -> void
+ getCurrentFloor() -> int
+ getDirection() -> Direction
The constructor initializes all elevators at the ground floor:
Elevator() {
this.currentFloor = 0 // all elevators start at ground floor
this.direction = IDLE
this.stops = new HashSet<>()
}
addStop is a unified interface used both by the controller (for hall calls) and by passengers (for destinations). The elevator doesn't care why it's stopping at floor 5. It just adds it to the queue.
step is where the movement logic lives. This is the heart of the elevator. The method that decides whether to move, stop, reverse, or go idle.
You might notice that ElevatorController.requestElevator(floor, direction) and Elevator.addStop(floor) both seem to "add a floor to something" and be tempted to extract a common interface like IStopHandler . This is a trap. These methods are doing fundamentally different things: requestElevator is a coordination operation that picks which elevator should handle a request, while addStop is a simple state mutation on a single elevator. Creating a shared interface would be forcing an abstraction that doesn't reflect any real polymorphic behavior. The controller is not a type of elevator, and they're not interchangeable. Just because two methods happen to accept similar parameters doesn't mean they should share an interface. Save interfaces for when you actually need polymorphism - when you have multiple implementations of the same behavior that need to be substitutable.
Implementation
Now that we've defined our classes, it's time to implement the actual method bodies. Before diving in, check with your interviewer - some want working code, others prefer pseudocode, and some just want you to talk through the logic. Adjust your level of detail based on what they're looking for. We'll stick to pseudocode, which is the most common, but will include complete implementations in common languages at the bottom of this section.
When implementing each method, we'll use this approach:
- Start with the main flow - What happens in the normal case when everything goes right?
- Handle the edge cases - What about invalid inputs, boundary conditions, or unexpected states?
We'll follow the same top-down approach from Class Design. The most interesting methods are ElevatorController.requestElevator() (dispatch logic) and Elevator.step() (the movement logic).
Aim for clarity over cleverness. Don't make things more complicated than they need to be.
ElevatorController
Let's implement requestElevator first.
Core logic
- Validate the floor number
- Pick which elevator should handle this request
- Tell that elevator to add the floor to its stops
Edge cases
- Floor out of bounds (less than 0 or greater than 9)
- Invalid direction
Here's a basic implementation:
void requestElevator(int floor, Direction direction) {
// Validate
if (floor < 0 || floor > 9) return // invalid floor
if (direction != UP && direction != DOWN) return // must specify up or down, idle is not a valid direction
// Find best elevator
Elevator best = selectBestElevator(floor, direction)
// Dispatch
best.addStop(floor)
}
The interesting part is selectBestElevator . There are multiple strategies here, and this is where we can have a good discussion with our interviewer about tradeoffs. Start with the simplest approach. After implementing it, proactively mention to your interviewer: "This works, but I could make it more sophisticated by considering direction. Would you like me to implement that?" This shows you're thinking ahead without over-engineering upfront.
The simplest strategy is to pick the elevator closest to the requested floor, regardless of which direction it's heading.
This is correct and takes about 30 seconds to implement. For a small building with light traffic, this works fine.
The problem becomes obvious with a simple example. Someone on floor 5 presses "up". The nearest elevator is on floor 6, but it's going down to floor 1. We'd send that elevator, even though it's heading the wrong way. The person on floor 5 would have to wait for the elevator to go all the way down to 1, then come back up to 5. This creates a poor user experience and inefficient elevator utilization.
An improvement is to consider direction but use a simple penalty score for elevators going the wrong way. We assign each elevator a score based on distance, but add a penalty (like +100) if the elevator is moving away from the requested floor.
This approach prioritizes idle elevators and elevators heading the right direction, while still considering elevators going the wrong way as a fallback.
While this works better than the naive approach, the penalty score (+100) is arbitrary and feels hacky. What if we have an elevator 2 floors away going the wrong direction versus one 150 floors away going the right direction? The scoring becomes unclear. The logic is also harder to reason about and tune. We're mixing different concerns (distance and direction) into a single score, which makes the priority ordering implicit rather than explicit.
The cleanest solution uses explicit priority tiers. We separate the selection logic into three helper methods and try them in order: elevators moving toward the floor, then idle elevators, then any elevator as a fallback.
The main selectBestElevator function read like plain English. It tries to find a "perfect match" first, then a nearest idle elevator, then the physically closest elevator.
This approach is readable, easy to follow, and aligns well with how real elevator control systems prioritize requests for minimal passenger wait times.
This approach has more code than the penalty score version, but the tradeoff is worth it. The logic is easier to understand, test, and modify. That said, there's still a limitation we haven't addressed: we don't check whether the elevator's existing stops will actually take it past the requested floor. For example, if an elevator is on floor 3 going UP with only a stop at floor 4, and someone requests floor 7, this considers it a "good match" even though the elevator will stop at 4 and likely reverse. A more sophisticated version would check if the elevator's queue extends past the requested floor.
In a real interview, you probably won't have time to implement even this version. What matters is showing you can see around corners. Call out the limitation, explain how you'd fix it if you had more time, then move on. Interviewers care more about your awareness of edge cases than whether you coded every detail. You may end up just implementing the selectBestElevator function and only explaining the helper methods.
We designed this for a single elevator scheduling algorithm. If you ran an elevator company serving different buildings with different needs — some prioritize wait time, others prioritize energy efficiency — you'd swap in different scheduling strategies. Each strategy implements the same selectBestElevator interface but picks elevators differently. That's the Strategy pattern in action.
The last method is step :
void step() {
for (Elevator e : elevators) {
e.step()
}
}
That's it. Just tell each elevator to advance one tick. The controller doesn't need to know how elevators move. That's encapsulated in Elevator.step() .
Elevator
Now that we've implemented the controller (the entry point), let's drill into the elevator movement logic. The heart of the elevator system is Elevator.step() . This is where we implement the movement behavior we discussed in requirements.
The behavior we're implementing (continue in one direction, reverse when no more stops ahead) is often called SCAN or the "elevator algorithm," but you don't need to know the name. It's similar to strategies used in real elevator control systems and in disk scheduling for operating systems. This approach is more efficient than FIFO (first-in-first-out) because it minimizes direction changes. Instead of jumping back and forth in the order requests arrive, we sweep through all requests in one direction before turning around.
What if I don't know the name of the algorithm? No problem at all! So long as you can explain the behavior and how it works, you're good to go.
Let's build it up piece by piece.
The happy path looks like this:
- If we have no stops, we're idle - do nothing
- If current floor is in our stops, stop here and remove it
- If we're idle but have stops, pick a direction based on where they are
- If no stops ahead in current direction, reverse
- Move one floor in current direction
That's the behavior in a nutshell. But the devil is in the details. Let's handle each case carefully.
Edge cases to handle
- Empty stops. Should go IDLE and do nothing
- Stopped at a floor. Need to remove it from stops, check if we should reverse or go idle
- IDLE with stops. Need to pick a direction
- No stops ahead in current direction. Need to reverse
- At floor boundaries (0 or 9). Can't move further
Here's the implementation:
void step() {
// Case 1: Nothing to do
if (stops.isEmpty()) {
direction = IDLE
return
}
// Case 2: Check if we should stop at current floor
if (stops.contains(currentFloor)) {
stops.remove(currentFloor)
// After stopping, check if we should go idle or reverse
if (stops.isEmpty()) {
direction = IDLE
return
}
if (!hasStopsInDirection(direction)) {
direction = (direction == UP) ? DOWN : UP
}
return // we stopped this tick, don't move
}
// Case 3: Need to move - but first make sure we're heading the right way
if (direction == IDLE) {
// Pick a direction by comparing current floor to any stop
int anyStop = stops.min() // just pick lowest floor to establish direction
direction = (anyStop > currentFloor) ? UP : DOWN
}
// Case 4: Reverse if no stops in current direction
if (!hasStopsInDirection(direction)) {
direction = (direction == UP) ? DOWN : UP
}
// Case 5: Move one floor
if (direction == UP) {
currentFloor++
} else if (direction == DOWN) {
currentFloor--
}
}
Let's walk through why each case matters:
Case 1: Nothing to do. If stops is empty, set direction to IDLE and return. Without this, an idle elevator with no stops would fall through to the movement logic and start drifting.
Case 2: Stopping at current floor. This is tricky. When we remove a stop, we need to immediately check whether we have more stops. If not, go IDLE. If yes, are any of them ahead? If not, reverse. Then return. We don't move on the same tick we stop. Some candidates forget the return and move the elevator after stopping, which creates weird behavior.
Notice we check for reversal right after stopping. This handles the case where we're on floor 7 going up, we stop at floor 7, and our only remaining stop is floor 3. We need to reverse immediately, not keep going up until we hit floor 9.
Case 3: Idle but have stops. If direction is IDLE but stops isn't empty, we need to pick a direction. The simplest approach is to compare our current floor to any stop (here we just pick the lowest). This establishes an initial direction. Once we start moving, Case 4 ensures we're heading toward actual stops.
Case 4: No stops ahead. This is the reversal logic. If we're going UP but all remaining stops are below us, reverse. If we're going DOWN but all remaining stops are above us, reverse. This also runs after we pick a direction in Case 3, ensuring we don't move away from our only stops.
Case 5: Move. Finally, if we're not idle, move one floor in the current direction.
Now let's implement the helper:
bool hasStopsInDirection(Direction dir) {
if (dir == UP) {
return stops.any(s -> s > currentFloor)
} else if (dir == DOWN) {
return stops.any(s -> s < currentFloor)
}
return false
}
This is straightforward. Do we have any stops above us (if going up) or below us (if going down)? This helper is what makes the reversal logic readable.
Notice we don't explicitly check for floors 0 and 9. Why not? Because hasStopsInDirection already handles it. If we're on floor 9 going UP, hasStopsInDirection(UP) returns false (no stops above floor 9 exist), so Case 4 reverses us to DOWN. If we're on floor 0 going DOWN, hasStopsInDirection(DOWN) returns false, so we reverse to UP. The boundary handling falls out naturally from the movement logic without needing special cases.
A common mistake is when candidates add explicit checks like "if currentFloor == 9 then direction = DOWN". This creates a bug. Imagine we're on floor 9 going up, and someone adds floor 7 to stops. We'd reverse to DOWN correctly. But if we have the explicit check, we'd force DOWN even when there might be a stop at floor 9 itself. Let the movement logic handle direction changes. Don't hardcode boundaries.
The last piece is addStop :
void addStop(int floor) {
if (floor < 0 || floor > 9) return // invalid floor
if (floor == currentFloor) return // already here
stops.add(floor)
}
Simple. Validate the floor number, don't add if we're already there, otherwise add to the set. The Set handles deduplication automatically.
Verification
Let's trace through a specific scenario to verify the movement algorithm works correctly. Walking through concrete examples like this helps catch logic bugs before your interviewer finds them.
Elevator is on floor 3, going UP, with stops at floors 5 and 7.
Tick 0: currentFloor=3, direction=UP, stops={5, 7}
- Not at a stop, move up
Tick 1: currentFloor=4, direction=UP, stops={5, 7}
- Not at a stop, move up
Tick 2: currentFloor=5, direction=UP, stops={5, 7}
- At floor 5! Remove it from stops
- stops={7}, still have stops ahead, stay UP
- Don't move this tick (we just stopped)
Tick 3: currentFloor=5, direction=UP, stops={7}
- Not at a stop, move up
Tick 4: currentFloor=6, direction=UP, stops={7}
- Not at a stop, move up
Tick 5: currentFloor=7, direction=UP, stops={7}
- At floor 7! Remove it from stops
- stops={}, no more stops, go IDLE
- Don't move this tick
Tick 6: currentFloor=7, direction=IDLE, stops={}
- No stops, stay idle
Now someone on floor 2 presses the call button going DOWN. The controller calls requestElevator(2, DOWN) , which dispatches to our elevator, adding floor 2 to stops.
Tick 7: currentFloor=7, direction=IDLE, stops={2}
- stops not empty, pick direction
- anyStop=2, which is < 7, so direction=DOWN
- hasStopsInDirection(DOWN)? Yes, floor 2 is below us
- Move down
Tick 8: currentFloor=6, direction=DOWN, stops={2}
- Not at a stop, move down
Tick 9: currentFloor=5, direction=DOWN, stops={2}
- Not at a stop, move down
...continues until reaching floor 2...
Notice how the elevator doesn't start moving until tick 7, after it receives the new request. The IDLE state is crucial.
from enum import Enum
class Direction(Enum):
UP = 1
DOWN = 2
IDLE = 3
class Elevator:
def __init__(self):
self.current_floor = 0
self.direction = Direction.IDLE
self.stops = set()
def add_stop(self, floor):
if floor < 0 or floor > 9:
return
if floor == self.current_floor:
return
self.stops.add(floor)
def step(self):
if not self.stops:
self.direction = Direction.IDLE
return
if self.current_floor in self.stops:
self.stops.remove(self.current_floor)
if not self.stops:
self.direction = Direction.IDLE
return
if not self.has_stops_in_direction(self.direction):
self.direction = Direction.DOWN if self.direction == Direction.UP else Direction.UP
return
if self.direction == Direction.IDLE:
any_stop = min(self.stops)
self.direction = Direction.UP if any_stop > self.current_floor else Direction.DOWN
if not self.has_stops_in_direction(self.direction):
self.direction = Direction.DOWN if self.direction == Direction.UP else Direction.UP
if self.direction == Direction.UP:
self.current_floor += 1
elif self.direction == Direction.DOWN:
self.current_floor -= 1
def has_stops_in_direction(self, dir):
if dir == Direction.UP:
return any(s > self.current_floor for s in self.stops)
elif dir == Direction.DOWN:
return any(s < self.current_floor for s in self.stops)
return False
def get_current_floor(self):
return self.current_floor
def get_direction(self):
return self.direction
Extensibility
If time allows, interviewers will sometimes add small twists to test whether our design can evolve cleanly. You typically won't need to fully implement these changes. Just explain how the classes would adapt. The depth and quantity of the extensibility follow-ups correlate with the candidate's target level (junior, mid-level, senior). Junior candidates often won't get any, mid-level may get one or two, and senior candidates may be asked to go into more depth.
If you're a junior engineer, feel free to skip this section and stop reading here! Only carry on if you're curious about the more advanced concepts.
Below are the most common ones for elevator systems, with more detail than you'd need in an actual interview.
1. "When someone on floor 5 presses 'up', but an elevator arrives going down, should it stop?"
Right now our design has a simplification. When we add a stop to an elevator, it doesn't remember whether that stop was for going up or down. If someone on floor 5 presses "up" and the elevator arrives going down, it would still stop.
The tradeoff is more accurate behavior, but the stop-checking logic gets slightly more complex. You'd also need to update addStop to take a direction parameter.
class Elevator:
- stops: Set<(int floor, Direction dir)> // Change: now stores floor AND direction
void addStop(int floor, Direction requestDirection) {
stops.add((floor, requestDirection)) // Store both pieces
}
void step() {
// Change: only stop if BOTH floor AND direction match
if (stops.contains((currentFloor, direction))) {
stops.remove((currentFloor, direction))
// ... rest of reversal/idle logic unchanged
}
}
2. "How would you add priority floors or an express elevator?"
Express elevators skip certain floors and only stop at major ones (like 0, 5, 9). Or maybe some VIP passengers get priority.
A response like this shows you're thinking about multiple options and can weigh between them, the key to senior+ design.
class Elevator:
- isExpress: bool
void addStop(int floor) {
if (isExpress && floor not in {0, 5, 9}) {
return // Reject non-express floors
}
stops.add(floor)
}
// In ElevatorController dispatch logic:
Elevator selectBestElevator(int floor, Direction dir) {
if (floor in {0, 5, 9}) {
return expressElevator // Send the express elevator
}
// ... normal selection logic for regular elevators
}
3. "How would you add undo to cancel a floor request?"
Maybe someone accidentally pressed the wrong floor button. Can they cancel it?
Fun fact: real elevators don't let you "undo" because a lit button signals to others that a stop is queued. Clearing it would silently drop requests from people who just trusted the light and didn't press again.
This works because our design already isolated state changes to a few methods. Adding removeStop is the inverse of addStop .
void removeStop(int floor) {
stops.remove(floor) // That's it - just remove from the set
}
What is Expected at Each Level?
Ok so what am I looking for at each level?
Junior
At the junior level, I want to see that you can model the basic entities and implement straightforward elevator movement. You should identify that you need an Elevator class to track position and direction, and some kind of controller to coordinate multiple elevators. The dispatch logic can be simple—nearest elevator is fine. For movement, I expect basic up/down behavior, and you might need hints to get the "continue in one direction, then reverse when clear" pattern right. It's okay if your elevator bounces back and forth inefficiently at first; the important thing is that it eventually services all requested floors. Edge cases like invalid floor numbers should be handled. If you can demonstrate a working simulation where I can request elevators, add destination floors, and step through time to see elevators move and stop correctly, you've met my expectations. Don't worry about optimizing the dispatch algorithm or handling complex concurrent scenarios.
Mid-level
For mid-level candidates, I expect the "keep going in one direction until no more stops, then reverse" behavior to be implemented correctly with minimal hints. The state machine should be clean: IDLE when no stops, UP or DOWN when moving, proper transitions between states. Your step() method should handle the subtle cases—stopping at a floor means you don't move that tick, checking for reversal happens after removing a stop, going IDLE when stops are empty. I'm also looking for reasonable dispatch logic. You don't need the priority tier approach, but you should recognize that the naive nearest-elevator strategy has problems and be able to discuss improvements. Your code should be organized so that ElevatorController handles coordination and Elevator handles its own movement. If I ask about extensibility, you should be able to discuss where changes would live—adding hall call direction handling means changing the stops data structure, not rewriting the movement algorithm.
Senior
Senior candidates should produce a design that demonstrates real systems thinking. You should ask the clarifying question about simulation vs. hardware control upfront—that signals you understand the distinction. The entity design should be tight: ElevatorController as stateless coordinator, Elevator with well-encapsulated movement logic. Your movement logic (continue in one direction, reverse when no more stops) should handle edge cases without me pointing them out: boundary floors, stopping and reversing on the same floor, going idle when empty. For dispatch, I want to see the priority tier approach or something equally sophisticated, with clear reasoning about why penalty scores are hacky. You should proactively mention the limitation of not checking whether existing stops will take the elevator past the requested floor. Strong senior candidates discuss tradeoffs fluently: immediate dispatch vs. request queuing, storing floor-only stops vs. floor-direction pairs, simple selection vs. predictive algorithms. If time permits, you should be able to sketch how the design would handle express elevators or priority floors without fundamentally restructuring the existing classes.