In the Previous attempt we designed an elevator system using two queues — an up queue and a down queue. But that system had some problems. It changed direction abruptly and did not respect the order of requests.

To overcome this, the system has been redesigned with an active and shadow queue strategy that better mirrors how a real elevator behaves.

The Requirements

The requirements remain the same as the original design:

  • A n-floor building, floors 1 through n. where n > 1
  • One elevator — single car, no fleet management
  • The elevator moves up or down
  • Doors only open when the elevator is idle at a floor
  • Door closes before the elevator starts moving, or after some time of being kept open
  • Requests come in two flavours:
    • Floor request — someone outside presses a direction button on a floor
    • Panel request — someone inside the car presses a destination floor
  • The algorithm must be efficient — minimum movement, maximum throughput
  • No starvation — every request eventually gets served

Planning

States and Directions

  • The elevator state model has been updated with a new maintenance state.
StateMeaning
idleParked, doors closed, waiting for work
movingTravelling between floors
openDoors open at a floor
closedDoors closing before departure
maintenanceElevator out of service, cannot move
  • Direction now includes a rest value — used when the elevator is idle or in maintenance and not pointing in any direction.
DirectionMeaning
upElevator is travelling upward
downElevator is travelling downward
restElevator is idle, no active direction

Elevator Current State

  • To track the elevator’s real-time status, a dedicated state class is introduced that holds the current floor, state, and direction together.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class ElevatorCurrentState {
  int currentFloor;
  ElevatorState currentState;
  ElevatorDirection currentDirection;

  ElevatorCurrentState({
    required this.currentFloor,
    required this.currentState,
    required this.currentDirection,
  });
}

Change Notifiers

  • Change notifiers are introduced to update state in a centralised, observable way. Any state mutation goes through these — no direct field writes scattered around the codebase.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void floorChangeNotifier(int newFloor) {
  state.currentFloor = newFloor;
  print("Current Floor: $newFloor");
}

void stateChangeNotifier(ElevatorState newState) {
  state.currentState = newState;
  print("Current State: $newState");
}

void directionChangeNotifier(ElevatorDirection newDirection) {
  state.currentDirection = newDirection;
  print("Current Direction: $newDirection");
}

Request Handling

  • There are two types of requests:

    • Floor request — originates outside the elevator. The person specifies which direction they want to go.
    • Panel request — originates inside the elevator. The person specifies only the destination floor.
  • Both delegate to a central task scheduler for processing.

Task Scheduler

  • The task scheduler is the decision layer between an incoming request and the queue system. Its responsibilities:

    • If the requested floor is the current floor — open and close the door immediately.
    • If the elevator is at rest — add to the active queue and set the direction.
    • If the request is in the same direction and ahead of the elevator — add to the active queue.
    • If the request is behind the elevator, or in the opposite direction — defer to the shadow queue.

Active Queue and Shadow Queue

  • The previous design used an up queue and a down queue. This version introduces a more flexible two-queue model:

    • Active queue — contains all floors that can be served in the elevator’s current sweep direction. Sorted so the nearest floor in the current direction is always first.
    • Shadow queue — holds requests that cannot be served in the current sweep. Once the active queue is drained, updateTasks() promotes eligible floors from the shadow queue into the active queue, reversing direction if needed.
  • This prevents the abrupt direction changes of the earlier design and ensures requests are served in a sensible, sweep-based order.

Code Walkthrough

Request Entry Points

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Future<void> floorRequestHandler({
  required int requestFromFloor,
  required ElevatorDirection requestDirection,
}) async {
  print(
    "Floor request received: floor $requestFromFloor, direction $requestDirection",
  );
  await taskScheduler(
    requestDirection: requestDirection,
    requestedFloor: requestFromFloor,
  );
}

Future<void> pannelRequestHandler({required int requestedFloor}) async {
  print("Panel request received: floor $requestedFloor");
  await taskScheduler(
    requestDirection: state.currentFloor > requestedFloor
        ? ElevatorDirection.down
        : ElevatorDirection.up,
    requestedFloor: requestedFloor,
  );
}
  • pannelRequestHandler infers direction from the difference between the current floor and the destination — no need for the user inside the car to specify direction.

The Core Move Loop

  • The move() function is the engine. It runs while either queue has entries, always working off the active queue:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
while (activeQueue.isNotEmpty || shadowQueue.isNotEmpty) {
  if (activeQueue.isEmpty) {
    updateTasks(); // Promote from shadow queue
  }

  int nextFloor = activeQueue.first;

  if (nextFloor == state.currentFloor) {
    await openDoor();
    activeQueue.removeAt(0);
    await closeDoor();
  } else {
    floorChangeNotifier(
      state.currentFloor +
          (state.currentDirection == ElevatorDirection.up ? 1 : -1),
    );
    await Future.delayed(Duration(milliseconds: 1000));
  }
}
  • The elevator moves one floor per iteration. On arrival it opens the door, removes the floor from the queue, and closes the door before continuing.

Promoting the Shadow Queue

  • When the active queue empties, updateTasks() decides which shadow queue entries to promote and what the new direction should be:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void updateTasks() {
  final hasAbove = shadowQueue.any((f) => f > state.currentFloor);
  final hasBelow = shadowQueue.any((f) => f < state.currentFloor);

  ElevatorDirection newDir;

  if (state.currentDirection == ElevatorDirection.up) {
    newDir = hasBelow ? ElevatorDirection.down : ElevatorDirection.up;
  } else if (state.currentDirection == ElevatorDirection.down) {
    newDir = hasAbove ? ElevatorDirection.up : ElevatorDirection.down;
  } else {
    newDir = hasAbove ? ElevatorDirection.up : ElevatorDirection.down;
  }

  final toServe = shadowQueue.where(
    (f) => newDir == ElevatorDirection.up
        ? f > state.currentFloor
        : f < state.currentFloor,
  ).toList();

  activeQueue.addAll(toServe);

  shadowQueue
    ..clear()
    ..addAll(shadowQueue.where((f) => !toServe.contains(f)));

  sortActive();
}

Maintenance Guard

  • Before any movement begins, the elevator checks if it is in maintenance mode:
1
2
3
4
if (currentState.currentState == ElevatorState.maintenance) {
  print("Elevator is under maintenance. Cannot move.");
  return;
}

What Changed From V1

V1 (Up Queue / Down Queue)V2 (Active / Shadow Queue)
Fixed up and down queuesDynamic active queue, direction-aware
Direction switched abruptlyDirection decided intelligently
No notion of elevator being at restElevatorDirection.rest introduced
No maintenance stateElevatorState.maintenance added
Request order not fully respectedShadow queue defers correctly

Full Code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
class Elevator {
  List<int> activeQueue = [];
  List<int> shadowQueue = [];

  final ElevatorCurrentState state = ElevatorCurrentState(
    currentFloor: 0,
    currentState: ElevatorState.idle,
    currentDirection: ElevatorDirection.rest,
  );

  Future<void> floorRequestHandler({
    required int requestFromFloor,
    required ElevatorDirection requestDirection,
  }) async {
    print(
      "Floor request received: floor $requestFromFloor, direction $requestDirection",
    );
    await taskScheduler(
      requestDirection: requestDirection,
      requestedFloor: requestFromFloor,
    );
  }

  Future<void> pannelRequestHandler({required int requestedFloor}) async {
    print("Panel request received: floor $requestedFloor");
    await taskScheduler(
      requestDirection: state.currentFloor > requestedFloor
          ? ElevatorDirection.down
          : ElevatorDirection.up,
      requestedFloor: requestedFloor,
    );
  }

  Future<void> taskScheduler({
    required int requestedFloor,
    required ElevatorDirection requestDirection,
  }) async {
    if (requestedFloor == state.currentFloor) {
      await openDoor();
      await closeDoor();
      return;
    }

    if (state.currentDirection == ElevatorDirection.rest) {
      activeQueue.add(requestedFloor);
      directionChangeNotifier(requestDirection);
      sortActive();
      print("Added floor $requestedFloor to active queue while at rest");
      return;
    }

    if (state.currentDirection == requestDirection) {
      if (state.currentFloor > requestedFloor) {
        addIfNotPresent(shadowQueue, requestedFloor);
      } else if (state.currentFloor < requestedFloor) {
        addIfNotPresent(activeQueue, requestedFloor);
      }
    } else {
      addIfNotPresent(shadowQueue, requestedFloor);
    }
  }

  void floorChangeNotifier(int newFloor) {
    state.currentFloor = newFloor;
    print("Current Floor: $newFloor");
  }

  void stateChangeNotifier(ElevatorState newState) {
    state.currentState = newState;
    print("Current State: $newState");
  }

  void directionChangeNotifier(ElevatorDirection newDirection) {
    state.currentDirection = newDirection;
    print("Current Direction: $newDirection");
  }

  Future<void> openDoor() async {
    stateChangeNotifier(ElevatorState.open);
    await Future.delayed(
      Duration(milliseconds: 600),
    ); // Simulate door opening time
  }

  Future<void> closeDoor() async {
    stateChangeNotifier(ElevatorState.closed);
    await Future.delayed(
      Duration(milliseconds: 600),
    ); // Simulate door closing time
  }

  void setIdle() {
    stateChangeNotifier(ElevatorState.idle);
    directionChangeNotifier(ElevatorDirection.rest);
  }

  Future<void> move() async {
    if (state.currentState == ElevatorState.maintenance) {
      print("Elevator is under maintenance. Cannot move.");
      return;
    }

    if (state.currentState == ElevatorState.open) {
      await closeDoor();
    }

    while (activeQueue.isNotEmpty || shadowQueue.isNotEmpty) {
      print(
        "Starting move cycle: activeQueue=$activeQueue, shadowQueue=$shadowQueue",
      );
      if (activeQueue.isEmpty) {
        updateTasks();
      }

      if (activeQueue.isEmpty) {
        setIdle();
        break;
      }

      int nextFloor = activeQueue.first;

      if (nextFloor == state.currentFloor) {
        print("Arrived at floor $nextFloor, opening doors");
        await openDoor();
        activeQueue.removeAt(0);
        await closeDoor();
      } else {
        // set direction if rest
        if (state.currentDirection == ElevatorDirection.rest) {
          directionChangeNotifier(
            nextFloor > state.currentFloor
                ? ElevatorDirection.up
                : ElevatorDirection.down,
          );
        }
        floorChangeNotifier(
          state.currentFloor +
              (state.currentDirection == ElevatorDirection.up ? 1 : -1),
        );
        await Future.delayed(
          Duration(milliseconds: 1000),
        ); // Simulate time to move between floors
      }
    }
  }

  void updateTasks() {
    if (shadowQueue.isEmpty) return;

    final hasAbove = shadowQueue.any((f) => f > state.currentFloor);
    final hasBelow = shadowQueue.any((f) => f < state.currentFloor);

    ElevatorDirection newDir;
    if (state.currentDirection == ElevatorDirection.up) {
      newDir = hasBelow ? ElevatorDirection.down : ElevatorDirection.up;
    } else if (state.currentDirection == ElevatorDirection.down) {
      newDir = hasAbove ? ElevatorDirection.up : ElevatorDirection.down;
    } else {
      newDir = hasAbove ? ElevatorDirection.up : ElevatorDirection.down;
    }

    directionChangeNotifier(newDir);

    final toServe = shadowQueue
        .where(
          (f) => newDir == ElevatorDirection.up
              ? f > state.currentFloor
              : f < state.currentFloor,
        )
        .toList();

    final remaining = shadowQueue
        .where(
          (f) => newDir == ElevatorDirection.up
              ? f < state.currentFloor
              : f > state.currentFloor,
        )
        .toList();

    activeQueue.addAll(toServe);
    shadowQueue
      ..clear()
      ..addAll(remaining);

    sortActive();
  }

  void sortActive() {
    if (state.currentDirection == ElevatorDirection.up) {
      activeQueue.sort();
    } else {
      activeQueue.sort((a, b) => b.compareTo(a));
    }
  }

  void addIfNotPresent(List<int> queue, int floor) {
    if (!queue.contains(floor)) {
      queue.add(floor);
      queue.sort();
    }
  }
}

Key Takeaway

  • The shift from a fixed two-queue model to an active + shadow queue simplifies responsibility—only one queue is actively processed at a time.
  • The elevator now behaves efficiently: it moves in one direction (sweeps), serves requests, and only reverses when needed, avoiding unnecessary back-and-forth.
  • The updateTasks() function drives this logic, acting as the core mechanism that manages queue transitions and direction flow.