Skip to main content
Low-Level Design · lesson 6 of 18
Open the Low-Level Design page →
Low-Level Design intermediate Lesson 6 of 18

Design an Elevator System: Scheduling Requests

Two floor requests, one elevator, and a nearest-request scheduler that has to decide which to serve first and print every floor it passes on the way.

By SkillByExample · Published

lld oop python elevator scheduling

One elevator, two pending requests — the scheduler has to decide which floor to serve first.

Nearest-request scheduling

class Elevator:
    def __init__(self):
        self.current_floor = 0
        self.requests = []

    def request_floor(self, floor):
        if floor == self.current_floor:
            print(f"Already at floor {floor}")
            return
        self.requests.append(floor)
        print(f"Request added: floor {floor}")

    def step(self):
        if not self.requests:
            print("No pending requests")
            return
        # nearest-request scheduling: always go to the closest pending floor
        target = min(self.requests, key=lambda f: abs(f - self.current_floor))
        self.requests.remove(target)
        direction = "up" if target > self.current_floor else "down"
        for floor in range(self.current_floor, target, 1 if target > self.current_floor else -1):
            next_floor = floor + (1 if target > self.current_floor else -1)
            print(f"Moving {direction}: passing floor {next_floor}")
        self.current_floor = target
        print(f"Arrived at floor {target}, doors open")

elevator = Elevator()
elevator.request_floor(5)
elevator.request_floor(2)
elevator.step()
elevator.step()
$ python elevator.py
Request added: floor 5
Request added: floor 2
Moving up: passing floor 1
Moving up: passing floor 2
Arrived at floor 2, doors open
Moving up: passing floor 3
Moving up: passing floor 4
Moving up: passing floor 5
Arrived at floor 5, doors open

Floor 5 was requested first, but the elevator serves floor 2 first, because it’s closer to the starting position (floor 0). The second step() call then continues on to the still-pending floor 5 request from wherever the elevator ended up — floor 2, not floor 0.

Practice

1. Confirm requesting the current floor doesn't add a request.
class Elevator:
    def __init__(self):
        self.current_floor = 0
        self.requests = []
    def request_floor(self, floor):
        if floor == self.current_floor:
            print(f"Already at floor {floor}")
            return
        self.requests.append(floor)
        print(f"Request added: floor {floor}")

e = Elevator()
e.request_floor(0)
$ python same_floor.py
Already at floor 0

No "Request added" line prints, and self.requests stays empty — the guard clause returns before appending anything.

2. Reject requests for floors outside the building's range.
class Elevator:
    MIN_FLOOR, MAX_FLOOR = 0, 10

    def __init__(self):
        self.current_floor = 0
        self.requests = []

    def request_floor(self, floor):
        if not (self.MIN_FLOOR <= floor <= self.MAX_FLOOR):
            print(f"Invalid floor {floor}: building has floors {self.MIN_FLOOR}-{self.MAX_FLOOR}")
            return
        if floor == self.current_floor:
            print(f"Already at floor {floor}")
            return
        self.requests.append(floor)
        print(f"Request added: floor {floor}")

e = Elevator()
e.request_floor(15)
e.request_floor(7)
$ python floor_bounds.py
Invalid floor 15: building has floors 0-10
Request added: floor 7

The bounds check runs before the “already here” check, since an out-of-range floor is invalid regardless of where the elevator currently is.

3. Drain the whole request queue with a loop instead of calling step() manually.
class Elevator:
    def __init__(self):
        self.current_floor = 0
        self.requests = []
    def request_floor(self, floor):
        if floor == self.current_floor:
            print(f"Already at floor {floor}")
            return
        self.requests.append(floor)
    def step(self):
        if not self.requests:
            print("No pending requests")
            return
        target = min(self.requests, key=lambda f: abs(f - self.current_floor))
        self.requests.remove(target)
        self.current_floor = target
        print(f"Arrived at floor {target}")

e = Elevator()
e.request_floor(3)
e.request_floor(1)
e.request_floor(6)
while e.requests:
    e.step()
e.step()
$ python drain_queue.py
Arrived at floor 1
Arrived at floor 3
Arrived at floor 6
No pending requests

while e.requests stops the loop the instant the list is empty, then one more manual step() call demonstrates the “nothing left to do” branch explicitly.

Next: a library system tracking which member has which book, and what “overdue” actually requires computing.

Frequently Asked Questions

Is 'nearest-request' the algorithm real elevators actually use?
No — real multi-floor buildings typically use SCAN/LOOK-style algorithms, where the elevator continues in its current direction serving every request along the way before reversing, rather than jumping to whichever single request happens to be closest right now. Nearest-request is simpler to reason about for a first LLD pass and is a legitimate starting point; naming SCAN as the next iteration is itself a good answer in an interview.
Why does request_floor(5) get served after request_floor(2) even though 5 was requested first?
The scheduler doesn't process requests in the order they arrived — `step()` picks whichever pending floor is numerically closest to `current_floor` (`min(self.requests, key=lambda f: abs(f - self.current_floor))`). Starting at floor 0, floor 2 is closer than floor 5, so it's served first regardless of request order.
What does the for loop with range(self.current_floor, target, ...) actually produce?
It doesn't print `current_floor` itself, only every floor strictly between the start and the target, in the direction of travel — the step argument is `1` for going up and `-1` for going down, which is why the loop and the printed 'passing floor' messages work symmetrically for upward and downward requests.
How would this design change for multiple elevators serving one set of floor calls?
That's a materially harder LLD problem — a Dispatcher class would need to decide *which* elevator answers each call (commonly by picking whichever elevator can reach the requested floor soonest given its current position and direction), on top of each elevator's own internal scheduling shown here. Worth attempting as a follow-on exercise once this single-elevator version is solid.