Overview

I've been asked a variety of questions in interviews over the years. They range from pretty standard behavioral questions to full on white boarding exercises. My favorite type of question however are the ones which ask you to theorize how you'd build something from scratch despite not knowing much about the inner workings of the subject matter. One question in particular I was asked a while back has stood out to me as a shining example of this.

If I were to task you with building an elevator, how would you do it?
An elevator? Like the software that powers the elevator?
Yeah! What would your approach be in regards to data structures, and what types of things would you think about?
Uhhhh.... I have no idea, lots of promises?

I find this question fascinating. I ride an elevator daily but I have no idea how they actually work. Because I have too much free time on my hands I decided to give this question far more attention than it probably deserves and decided to do a deep dive on it.

Disclaimer: The company and scenario in this post are completely fake. If you actually make software for elevators please don't use this code or post as an example. I have no experience writing software for elevators and I don't want to be responsible for hurting anyone.

Welcome to the Team

Let's set the scene. You're a software engineer who has just finished your onboarding for "Elevated Industries", a startup who specializes in building and installing elevators. Somewhere in the hiring process they neglected to tell you that you'll be their only developer and that you'll be the first person to write software for their elevators.

Elevated
Industries

Write the Elevator Software

Jira story iconJira priority icon13
GitHub profile iconAPP-1337

Seeing as the longevity of the entire company is now resting on your shoulders it's probably best you start planning your approach. Before we start writing code we need to consider the following items so we have an idea of what we need to build.

We'll also need to select a language to build this with. Something like C or Java would be a good choice but I'm going to throw caution to the wind and use TypeScript instead.

Going Up

Let's build our basic controls so we can interface with our elevators mechanics. Due to the nature of an elevator our code is going to need to be relatively synchronous, each action should depend on another and everything should work in tandem. In order to achieve that we'll create an enum of status codes that we can reference.

constants.ts
enum Status = {
  // Used when the elevator is idle and not moving.
  IDLE = 'IDLE',
  // Used when the elevator has stopped at a current floor, but has a request to move up/down.
  IDLE_PENDING = 'IDLE_PENDING',
  // The elevator is moving up.
  MOVING_UP = 'MOVING_UP',
  // The elevator is moving down.
  MOVING_DOWN = 'MOVING_DOWN'
}

enum Direction = {
  UP = 'UP',
  DOWN = 'DOWN'
}

We're going to need to create a method for moving the elevator and telling the mechanical operation to perform, for this example we'll assume that the mechanicial function resolves a promise when the elevator is done performing a mechanical task. We'll also need a method that updates the current status as the elevator passes each floor in the building.

In the below example whenever a floor is requested using the moveElevator method we check to see if our elevator is in the IDLE status before performing the mechanical task. The floor is increased or decreased by 1 depending on the direction the elevator is traveling in.

elevator.ts
class ElevatorController {
  constructor() {
    // Contains the current operation status of the elevator.
    this.state = {
      currentFloor: 0,
      status: Status.IDLE,
      requestedFloor: '',
    }

    this.maxFloors = 2
    this.setCurrentFloor = this.setCurrentFloor.bind(this)
    this.moveElevator = this.moveElevator.bind(this)
    this.setState = this.setState.bind(this);
  }

  async setState(state: {
   [key: string]: any
   }): Promise<void> {
    try {
      this.state = Object.assign(this.state, state);
    } catch () {
      // Some sort of error handling would probbaly be a good idea here.
    }
  }

  async setCurrentFloor(requestedFloor: number): void {
    const currentFloor =
      requestedFloor > this.state.currentFloor
        ? this.state.currentFloor + 1
        : this.state.currentFloor - 1

   await this.setState({
      currentFloor,
      status:
        this.state.currentFloor === requestedFloor
          ? this.state.status
          : Status.IDLE,
      requestedFloor:
        this.state.currentFloor === requestedFloor ? requestedFloor : '',
    })

    /* If this isn't the floor that was requested then we'll move to the next one. */
    if (this.state.currentFloor !== requestedFloor) {
      this.moveElevator(requestedFloor)
    }
  }

  async moveElevator(requestedFloor: number): void {
    if (
      this.state.status === Status.IDLE &&
      this.state.currentFloor !== requestedFloor
    ) {
      await this.setState({
        requestedFloor,
        status:
          requestedFloor !== this.state.currentFloor &&
          requestedFloor > this.state.currentFloor
            ? Status.MOVING_UP
            : Status.MOVING_DOWN,
      })

      // Simulates the cab traveling through the elevator shaft.
      await mechanical()

      this.setCurrentFloor(requestedFloor)
    }
  }
}

Utilizing promises and status codes make this a breeze. Once the elevator returns to an IDLE state we can then perform any followup updates to our ElevatorController with the status of a new operation such as its travel direction and its current floor. This is a pretty great start and so far we have a working prototype!

Panic at the Elevator

"Elevated Industries" doesn't believe in internal testing and decided it was a great idea to deploy our work in progress code to a production elevator before it was ready.

Newspaper
Article

People aren't happy, and the office is chaos. Let's take another look at what we're missing.

Let's re-visit our code and make some refactors. We're going to need to hold onto the requested floors as an array, we'll also need to create a new method that requests a floor. This method should simply push the requested floor into our requested floors array and then call the moveElevator method. The moveElevator method should only be called once if the elevator is idle, this is because this function will be recursive and will keep calling itself if anything exists in the newly created array.

elevator.ts
async requestFloor(requestedFloor: number): void {
  /* Ensures that we're not requesting the floor that we're currently on. */
  if (requestedFloor !== this.state.currentFloor) {
    const requestedFloors = Array.from(this.state.requestedFloors)

    /* Prevents the same floor from being requested multiple times. */
    if (!requestedFloors.includes(requestedFloor)) {
      requestedFloors.push(requestedFloor)
    }

    await this.setState(
      {
        requestedFloors,
      }
    )

    /* Prevents people from button mashing the elevator controls and breaking stuff accidently. */
    if (this.state.status === Status.IDLE) {
      this.moveElevator()
    }
  }
}

We'll also need to make some changes to our moveElevator function. As it needs to be smarter about which direction it's traveling in we'll need to filter the requested floors based on which direction it's currently going. As the elevator status is reset to IDLE after every call we have to hold onto the previous status to inform the next run if it should pull from the ascending or descending floor array.

We'll then use this data to tell our setCurrentFloor method which floor to travel towards. It's not pretty but it does the job.

elevator.ts
async moveElevator(): void {
  // We filter the ascending/descending floors into their own arrays.
  const ascendingFloors = this.state.requestedFloors.filter(
    item => item > this.state.currentFloor
  )
  const descendingFloors = this.state.requestedFloors.filter(
    item => item < this.state.currentFloor
  )

  /* Checks the previous status to determine the next floor the elevator
    should travel to. The elevator only really cares if it's ascending or descending, it makes a check on every floor to see if it needs to open. */
  const nextFloor =
    this.state.previousStatus === Status.MOVING_UP && ascendingFloors.length
      ? ascendingFloors[0]
      : this.state.previousStatus === Status.MOVING_DOWN &&
        descendingFloors.length
      ? descendingFloors[0]
      : this.state.requestedFloors[0]

  /* The IDLE status ensures that the elevator is ready to move again.
  This is a safety check to prevent the elevator from moving if it's not ready. */
  if (typeof nextFloor !== 'undefined' && this.state.status === Status.IDLE) {
    this.setState(
      {
        status:
          nextFloor > this.state.currentFloor
            ? Status.MOVING_UP
            : Status.MOVING_DOWN,
      }
    )

    if (this.state.currentFloor !== nextFloor) {
      await sleep()
      this.setCurrentFloor(
        nextFloor > this.state.currentFloor ? Direction.UP : Direction.DOWN
      )
    }
  }
}

Each time the setCurrentFloor function is called we'll remove the current floor from the requestedFloors array and set the status to IDLE_PENDING to inform the doors they should be opened. We can then check to see if more requested floors exist. In order to keep the operations in sync with each other we'll set the status to IDLE just before calling the moveElevator method again.

You can find the code below for the full ElevatorController class.

elevator.ts
class ElevatorController {
  constructor() {
    this.state = {
      previousStatus: null,
      status: Status.IDLE,
      currentFloor: 0,
      requestedFloors: [],
    }

    this.maxFloors = 4
    this.setState = this.setState.bind(this)
    this.setCurrentFloor = this.setCurrentFloor.bind(this)
    this.moveElevator = this.moveElevator.bind(this)
    this.requestFloor = this.requestFloor.bind(this)
  }

  async setState(state: {
   [key: string]: any
  }): Promise<void> {
    try {
      this.state = Object.assign(this.state, state);
    } catch () {
      // Some sort of error handling would probbaly be a good idea here.
    }
  }

  async setCurrentFloor(upOrDown: Direction): void {
    const requestedFloors = Array.from(this.state.requestedFloors)
    const currentFloor =
      upOrDown === Direction.UP
        ? this.state.currentFloor + 1
        : this.state.currentFloor - 1

    const status = !requestedFloors.includes(currentFloor)
      ? this.state.status
      : Status.IDLE_PENDING

    if (requestedFloors.includes(currentFloor)) {
      requestedFloors.splice(requestedFloors.indexOf(currentFloor), 1)
    }

    await this.setState({
      currentFloor,
      previousStatus: this.state.status,
      status,
      requestedFloors,
    })

    // Simulates the door opening.
    await mechanical()

    /* Resets the status back to idle before moving the elevator again.
      If we're done moving the elevator the previousStatus is reset. */
    await this.setState({
      status: Status.IDLE,
      previousStatus: this.state.requestedFloors.length ? this.state.previousState : null
    })

    if (this.state.requestedFloors.length) {
      this.moveElevator()
    }
  }

  async moveElevator(): void {
    const ascendingFloors = this.state.requestedFloors.filter(
      item => item > this.state.currentFloor
    )
    const descendingFloors = this.state.requestedFloors.filter(
      item => item < this.state.currentFloor
    )

    const nextFloor =
      this.state.previousStatus === Status.MOVING_UP && ascendingFloors.length
        ? ascendingFloors[0]
        : this.state.previousStatus === Status.MOVING_DOWN &&
          descendingFloors.length
        ? descendingFloors[0]
        : this.state.requestedFloors[0]

    if (typeof nextFloor !== 'undefined' && this.state.status === Status.IDLE) {
      await this.setState({
        status:
          nextFloor > this.state.currentFloor
            ? Status.MOVING_UP
            : Status.MOVING_DOWN,
      })

      if (this.state.currentFloor !== nextFloor) {
        // Simulates the cab traveling through the elevator shaft.
        await mechanical()

        this.setCurrentFloor(
          nextFloor > this.state.currentFloor ? Direction.UP : Direction.DOWN
        )
      }
    }
  }

  async requestFloor(requestedFloor: number): void {
    if (requestedFloor !== this.state.currentFloor) {
      const requestedFloors = Array.from(this.state.requestedFloors)

      if (!requestedFloors.includes(requestedFloor)) {
        requestedFloors.push(requestedFloor)
      }

      await this.setState(
        {
          requestedFloors,
        }
      )

      if (this.state.status === Status.IDLE) {
        this.moveElevator()
      }
    }
  }
}

Let's increase the number of available floors and try this again. Try requesting floors in a random order, you'll notice that the elevator keeps going in the direction it was previously traveling in before changing.

With this working prototype we've saved the companies reputation and we're hailed as a hero internally, at least until something goes wrong. If this was a real project we probably should of prioritized error handling as a single error will cause people to get stuck.

So yeah we'll need to prioritize an emergency door release for next sprint, we have 5 people stuck in an elevator and mechanical engineering says its a software problem.
lol

Closing Notes

I'm sure there's a million ways to solve this problem, and there's likely a million different ways this code could be re-factored to make it more efficient. Thanks for reading!