Part 2: implementing nearest-elevator dispatch and SCAN-style scheduling, merging new requests into an in-progress route, and coordinating multiple elevators.
Published September 23, 2026
Building on Elevator System — Requirements & Class Design's state machine, this part implements the dispatch decision left as a stub in Part 1.
The simplest reasonable policy: when an external call comes in, send whichever elevator can reach that floor soonest.
interface DispatchStrategy {
Elevator selectElevator(List<Elevator> elevators, int requestFloor, Direction requestDirection);
}
class NearestElevatorStrategy implements DispatchStrategy {
public Elevator selectElevator(List<Elevator> elevators, int requestFloor, Direction requestDirection) {
return elevators.stream()
.filter(e -> isSuitable(e, requestFloor, requestDirection)) // don't send a downward-moving elevator to answer an upward call it would have to pass
.min(Comparator.comparingInt(e -> Math.abs(e.getCurrentFloor() - requestFloor)))
.orElseGet(() -> elevators.stream().min(Comparator.comparingInt(e -> Math.abs(e.getCurrentFloor() - requestFloor))).get());
}
private boolean isSuitable(Elevator e, int requestFloor, Direction requestDirection) {
if (e.getState() instanceof IdleState) return true;
boolean movingTowardRequest = (e.getDirection() == Direction.UP && e.getCurrentFloor() <= requestFloor)
|| (e.getDirection() == Direction.DOWN && e.getCurrentFloor() >= requestFloor);
return movingTowardRequest && e.getDirection() == requestDirection; // same direction AND already heading that way
}
}
Notice the deliberate two-tier filter: first prefer an elevator that's idle or already heading the right way past the requested floor (so it can pick up the passenger without a detour); only fall back to "closest regardless of direction" if no suitable candidate exists. This is exactly the real-world behavior of not sending an elevator that just passed your floor going the wrong way, even if it's technically nearest.
Nearest-elevator-first optimizes each individual request's wait time but can cause an elevator to zigzag inefficiently across floors serving requests in whatever order they arrive. SCAN scheduling instead has each elevator continue in its current direction, picking up every request along the way, until no more requests exist in that direction — then reverses. This is directly analogous to a disk-scheduling algorithm of the same name, and it's what the upRequests/downRequests split-TreeSet design from Part 1 already sets up naturally: an elevator moving up keeps consuming upRequests in ascending order until empty, then switches to consuming downRequests, rather than jumping to whichever individual request happens to be "nearest" next.
class Elevator {
// ...fields from Part 1...
void addExternalRequest(int floor, Direction direction) {
if (direction == Direction.UP) upRequests.add(floor);
else downRequests.add(floor);
// No need to interrupt or replan anything — the TreeSet naturally re-sorts,
// and MovingUpState/MovingDownState will pick it up on a later floor if it's ahead
}
}
Because pending requests live in a sorted set rather than an ordered queue/list, a newly-arrived request simply inserts into its correct position — if the elevator is moving up and a new request arrives for a floor still ahead of it, the elevator picks it up along the way with zero special-casing. This is the payoff of the Part 1 data-structure choice: merging a new request into an in-progress route isn't a separate algorithm, it falls out of the existing structure for free.
class ElevatorSystem {
private final List<Elevator> elevators;
private final DispatchStrategy dispatchStrategy;
ElevatorSystem(List<Elevator> elevators, DispatchStrategy dispatchStrategy) {
this.elevators = elevators;
this.dispatchStrategy = dispatchStrategy;
}
void requestElevator(int floor, Direction direction) {
Elevator chosen = dispatchStrategy.selectElevator(elevators, floor, direction);
chosen.addExternalRequest(floor, direction);
}
}
ElevatorSystem delegates the actual selection decision to DispatchStrategy — the same Strategy-pattern separation used for Parking Lot's spot allocation. This isolation matters specifically for multi-elevator coordination: swapping NearestElevatorStrategy for a load-balancing strategy (favor the elevator with the fewest pending requests, to spread load evenly across the bank rather than always converging on whichever is geometrically closest) means writing one new class, with zero changes to ElevatorSystem or Elevator.
Q: What's a concrete downside of pure nearest-elevator-first at scale? A: Under sustained load, it can concentrate requests on whichever elevators happen to start closest to high-traffic floors, leaving others comparatively idle — a load-aware or hybrid strategy (distance weighted against current queue depth) avoids this by explicitly considering both factors, not just proximity.
Q: How would you handle an elevator going out of service mid-route, with passengers' requests already queued?
A: The system-level ElevatorSystem would need to detect the failure, redistribute that elevator's pending requests to other elevators via the same DispatchStrategy used for new requests, and the failed elevator's own state machine would transition to a new 'OutOfService' state that rejects step() calls — this is a natural extension of the existing State pattern structure, not a redesign.
Q: Why filter to 'same direction AND already heading that way' rather than just 'closest, period' in NearestElevatorStrategy? A: Sending the geometrically closest elevator regardless of its direction can mean it has to travel past the requester, reverse, and come back — often slower in wall-clock time than a slightly farther elevator already heading the correct direction. This is the same 'don't optimize the wrong metric' lesson as elsewhere in system design: proximity in floors isn't the same as proximity in actual wait time.