Code · LLD machine coding · PhonePe prep
Elevator System
Design a controller for N elevators in a building.
Question
Design a controller for N elevators in a building.
- External requests: a person on floor F presses UP or DOWN.
- Internal requests: a person inside elevator E presses a floor.
- Dispatch each external request to the best elevator (nearest here, and the rule is swappable).
- Each elevator serves stops in its current direction before turning around.
- Displays show each elevator’s floor and direction as it moves.
Design: State pattern (Idle / MovingUp / MovingDown), Strategy for elevator selection, Observer for displays,
a singleton ElevatorSystem, one thread per elevator. Run ElevatorSystemDemo.
Elevator.java 74 lines GitHub ↗
package com.practice.phonepe.questions.elevatorsystem;
import com.practice.phonepe.questions.elevatorsystem.enums.Direction;
import com.practice.phonepe.questions.elevatorsystem.models.Request;
import com.practice.phonepe.questions.elevatorsystem.observer.ElevatorObserver;
import com.practice.phonepe.questions.elevatorsystem.state.ElevatorState;
import com.practice.phonepe.questions.elevatorsystem.state.IdleState;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;
public class Elevator implements Runnable {
private final int id;
private AtomicInteger currentFloor;
private ElevatorState state;
private volatile boolean isRunning = true;
private final TreeSet<Integer> upRequests;
private final TreeSet<Integer> downRequests;
// Observer Pattern: List of observers
private final List<ElevatorObserver> observers = new ArrayList<>();
public Elevator(int id) {
this.id = id;
this.currentFloor = new AtomicInteger(1);
this.upRequests = new TreeSet<>();
this.downRequests = new TreeSet<>((a, b) -> b - a);
this.state = new IdleState();
}
// --- Observer Pattern Methods ---
public void addObserver(ElevatorObserver observer) {
observers.add(observer);
observer.update(this); // Send initial state
}
public void notifyObservers() {
for (ElevatorObserver observer : observers) {
observer.update(this);
}
}
// --- State Pattern Methods ---
public void setState(ElevatorState state) {
this.state = state;
notifyObservers(); // Notify observers on direction change
}
public void move() {
state.move(this);
}
// --- Request Handling ---
public synchronized void addRequest(Request request) {
System.out.println("Elevator " + id + " processing: " + request);
state.addRequest(this, request);
}
// --- Getters and Setters ---
public int getId() { return id; }
public int getCurrentFloor() { return currentFloor.get(); }
public void setCurrentFloor(int floor) {
this.currentFloor.set(floor);
notifyObservers(); // Notify observers on floor change
}
public Direction getDirection() { return state.getDirection(); }
public TreeSet<Integer> getUpRequests() { return upRequests; }
public TreeSet<Integer> getDownRequests() { return downRequests; }
public boolean isRunning() { return isRunning; }
public void stopElevator() { this.isRunning = false; }
@Override
public void run() {
while (isRunning) {
move();
try {
Thread.sleep(1000); // Simulate movement time
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
isRunning = false;
}
}
}
} ElevatorSystem.java 74 lines GitHub ↗
package com.practice.phonepe.questions.elevatorsystem;
import com.practice.phonepe.questions.elevatorsystem.enums.Direction;
import com.practice.phonepe.questions.elevatorsystem.enums.RequestSource;
import com.practice.phonepe.questions.elevatorsystem.models.Request;
import com.practice.phonepe.questions.elevatorsystem.observer.ElevatorDisplay;
import com.practice.phonepe.questions.elevatorsystem.strategy.ElevatorSelectionStrategy;
import com.practice.phonepe.questions.elevatorsystem.strategy.NearestElevatorStrategy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
public class ElevatorSystem {
private static ElevatorSystem instance;
private final Map<Integer, Elevator> elevators;
private final ElevatorSelectionStrategy selectionStrategy;
private final ExecutorService executorService;
private ElevatorSystem(int numElevators) {
this.selectionStrategy = new NearestElevatorStrategy();
this.executorService = Executors.newFixedThreadPool(numElevators);
List<Elevator> elevatorList = new ArrayList<>();
ElevatorDisplay elevatorDisplay = new ElevatorDisplay(); // Create the observer
for (int i = 1; i <= numElevators; i++) {
Elevator elevator = new Elevator(i);
elevator.addObserver(elevatorDisplay); // Attach the observer
elevatorList.add(elevator);
}
this.elevators = elevatorList.stream().collect(Collectors.toMap(Elevator::getId, e -> e));
}
public static synchronized ElevatorSystem getInstance(int numElevators) {
if (instance == null) {
instance = new ElevatorSystem(numElevators);
}
return instance;
}
public void start() {
for (Elevator elevator : elevators.values()) {
executorService.submit(elevator);
}
}
// --- Facade Methods ---
// EXTERNAL Request (Hall Call)
public void requestElevator(int floor, Direction direction) {
System.out.println("\n>> EXTERNAL Request: User at floor " + floor + " wants to go " + direction);
Request request = new Request(floor, direction, RequestSource.EXTERNAL);
// Use strategy to find the best elevator
Optional<Elevator> selectedElevator = selectionStrategy.selectElevator(new ArrayList<>(elevators.values()), request);
if (selectedElevator.isPresent()) {
selectedElevator.get().addRequest(request);
} else {
System.out.println("System busy, please wait.");
}
}
// INTERNAL Request (Cabin Call)
public void selectFloor(int elevatorId, int destinationFloor) {
System.out.println("\n>> INTERNAL Request: User in Elevator " + elevatorId + " selected floor " + destinationFloor);
Request request = new Request(destinationFloor, Direction.IDLE, RequestSource.INTERNAL);
Elevator elevator = elevators.get(elevatorId);
if (elevator != null) {
elevator.addRequest(request);
} else {
System.err.println("Invalid elevator ID.");
}
}
public void shutdown() {
System.out.println("Shutting down elevator system...");
for (Elevator elevator : elevators.values()) {
elevator.stopElevator();
}
executorService.shutdown();
}
} enums/Direction.java 4 lines GitHub ↗
package com.practice.phonepe.questions.elevatorsystem.enums;
public enum Direction {
UP, DOWN, IDLE
} enums/RequestSource.java 5 lines GitHub ↗
package com.practice.phonepe.questions.elevatorsystem.enums;
public enum RequestSource {
INTERNAL, // From inside the cabin
EXTERNAL // From the hall/floor
} models/Request.java 27 lines GitHub ↗
package com.practice.phonepe.questions.elevatorsystem.models;
import com.practice.phonepe.questions.elevatorsystem.enums.Direction;
import com.practice.phonepe.questions.elevatorsystem.enums.RequestSource;
public class Request {
private final int targetFloor;
private final Direction direction; // Primarily for External requests
private final RequestSource source;
public Request(int targetFloor, Direction direction, RequestSource source) {
this.targetFloor = targetFloor;
this.direction = direction;
this.source = source;
}
public int getTargetFloor() {
return targetFloor;
}
public Direction getDirection() {
return direction;
}
public RequestSource getSource() {
return source;
}
@Override
public String toString() {
return source + " Request to floor " + targetFloor +
(source == RequestSource.EXTERNAL ? " going " + direction : "");
}
} observer/ElevatorDisplay.java 10 lines GitHub ↗
package com.practice.phonepe.questions.elevatorsystem.observer;
import com.practice.phonepe.questions.elevatorsystem.Elevator;
public class ElevatorDisplay implements ElevatorObserver {
@Override
public void update(Elevator elevator) {
System.out.println("[DISPLAY] Elevator " + elevator.getId() +
" | Current Floor: " + elevator.getCurrentFloor() +
" | Direction: " + elevator.getDirection());
}
} observer/ElevatorObserver.java 5 lines GitHub ↗
package com.practice.phonepe.questions.elevatorsystem.observer;
import com.practice.phonepe.questions.elevatorsystem.Elevator;
public interface ElevatorObserver {
void update(Elevator elevator);
} state/ElevatorState.java 9 lines GitHub ↗
package com.practice.phonepe.questions.elevatorsystem.state;
import com.practice.phonepe.questions.elevatorsystem.Elevator;
import com.practice.phonepe.questions.elevatorsystem.enums.Direction;
import com.practice.phonepe.questions.elevatorsystem.models.Request;
public interface ElevatorState {
void move(Elevator elevator);
void addRequest(Elevator elevator, Request request);
Direction getDirection();
} state/IdleState.java 26 lines GitHub ↗
package com.practice.phonepe.questions.elevatorsystem.state;
import com.practice.phonepe.questions.elevatorsystem.Elevator;
import com.practice.phonepe.questions.elevatorsystem.enums.Direction;
import com.practice.phonepe.questions.elevatorsystem.models.Request;
public class IdleState implements ElevatorState {
@Override
public void move(Elevator elevator) {
if (!elevator.getUpRequests().isEmpty()) {
elevator.setState(new MovingUpState());
} else if (!elevator.getDownRequests().isEmpty()) {
elevator.setState(new MovingDownState());
}
// Else stay idle
}
@Override
public void addRequest(Elevator elevator, Request request) {
if (request.getTargetFloor() > elevator.getCurrentFloor()) {
elevator.getUpRequests().add(request.getTargetFloor());
} else if (request.getTargetFloor() < elevator.getCurrentFloor()) {
elevator.getDownRequests().add(request.getTargetFloor());
}
// If request is for current floor, doors would open (handled implicitly by moving to that floor)
}
@Override
public Direction getDirection() { return Direction.IDLE; }
} state/MovingDownState.java 43 lines GitHub ↗
package com.practice.phonepe.questions.elevatorsystem.state;
import com.practice.phonepe.questions.elevatorsystem.Elevator;
import com.practice.phonepe.questions.elevatorsystem.enums.Direction;
import com.practice.phonepe.questions.elevatorsystem.enums.RequestSource;
import com.practice.phonepe.questions.elevatorsystem.models.Request;
public class MovingDownState implements ElevatorState {
@Override
public void move(Elevator elevator) {
if (elevator.getDownRequests().isEmpty()) {
elevator.setState(new IdleState());
return;
}
Integer nextFloor = elevator.getDownRequests().first();
elevator.setCurrentFloor(elevator.getCurrentFloor() - 1);
if (elevator.getCurrentFloor() == nextFloor) {
System.out.println("Elevator " + elevator.getId() + " stopped at floor " + nextFloor);
elevator.getDownRequests().pollFirst();
}
if (elevator.getDownRequests().isEmpty()) {
elevator.setState(new IdleState());
}
}
@Override
public void addRequest(Elevator elevator, Request request) {
// Internal requests always get added to the appropriate queue
if (request.getSource() == RequestSource.INTERNAL) {
if (request.getTargetFloor() > elevator.getCurrentFloor()) {
elevator.getUpRequests().add(request.getTargetFloor());
} else {
elevator.getDownRequests().add(request.getTargetFloor());
}
return;
}
// External requests
if (request.getDirection() == Direction.DOWN && request.getTargetFloor() <= elevator.getCurrentFloor()) {
elevator.getDownRequests().add(request.getTargetFloor());
} else if (request.getDirection() == Direction.UP) {
elevator.getUpRequests().add(request.getTargetFloor());
}
}
@Override
public Direction getDirection() { return Direction.DOWN; }
} state/MovingUpState.java 43 lines GitHub ↗
package com.practice.phonepe.questions.elevatorsystem.state;
import com.practice.phonepe.questions.elevatorsystem.Elevator;
import com.practice.phonepe.questions.elevatorsystem.enums.Direction;
import com.practice.phonepe.questions.elevatorsystem.enums.RequestSource;
import com.practice.phonepe.questions.elevatorsystem.models.Request;
public class MovingUpState implements ElevatorState {
@Override
public void move(Elevator elevator) {
if (elevator.getUpRequests().isEmpty()) {
elevator.setState(new IdleState());
return;
}
Integer nextFloor = elevator.getUpRequests().first();
elevator.setCurrentFloor(elevator.getCurrentFloor() + 1);
if (elevator.getCurrentFloor() == nextFloor) {
System.out.println("Elevator " + elevator.getId() + " stopped at floor " + nextFloor);
elevator.getUpRequests().pollFirst();
}
if (elevator.getUpRequests().isEmpty()) {
elevator.setState(new IdleState());
}
}
@Override
public void addRequest(Elevator elevator, Request request) {
// Internal requests always get added to the appropriate queue
if (request.getSource() == RequestSource.INTERNAL) {
if (request.getTargetFloor() > elevator.getCurrentFloor()) {
elevator.getUpRequests().add(request.getTargetFloor());
} else {
elevator.getDownRequests().add(request.getTargetFloor());
}
return;
}
// External requests
if (request.getDirection() == Direction.UP && request.getTargetFloor() >= elevator.getCurrentFloor()) {
elevator.getUpRequests().add(request.getTargetFloor());
} else if (request.getDirection() == Direction.DOWN) {
elevator.getDownRequests().add(request.getTargetFloor());
}
}
@Override
public Direction getDirection() { return Direction.UP; }
} strategy/ElevatorSelectionStrategy.java 8 lines GitHub ↗
package com.practice.phonepe.questions.elevatorsystem.strategy;
import com.practice.phonepe.questions.elevatorsystem.Elevator;
import com.practice.phonepe.questions.elevatorsystem.models.Request;
import java.util.List;
import java.util.Optional;
public interface ElevatorSelectionStrategy {
Optional<Elevator> selectElevator(List<Elevator> elevators, Request request);
} strategy/NearestElevatorStrategy.java 34 lines GitHub ↗
package com.practice.phonepe.questions.elevatorsystem.strategy;
import com.practice.phonepe.questions.elevatorsystem.Elevator;
import com.practice.phonepe.questions.elevatorsystem.enums.Direction;
import com.practice.phonepe.questions.elevatorsystem.models.Request;
import java.util.List;
import java.util.Optional;
public class NearestElevatorStrategy implements ElevatorSelectionStrategy {
@Override
public Optional<Elevator> selectElevator(List<Elevator> elevators, Request request) {
Elevator bestElevator = null;
int minDistance = Integer.MAX_VALUE;
for (Elevator elevator : elevators) {
if (isSuitable(elevator, request)) {
int distance = Math.abs(elevator.getCurrentFloor() - request.getTargetFloor());
if (distance < minDistance) {
minDistance = distance;
bestElevator = elevator;
}
}
}
return Optional.ofNullable(bestElevator);
}
private boolean isSuitable(Elevator elevator, Request request) {
if (elevator.getDirection() == Direction.IDLE)
return true;
if (elevator.getDirection() == request.getDirection()) {
if (request.getDirection() == Direction.UP && elevator.getCurrentFloor() <= request.getTargetFloor())
return true;
if (request.getDirection() == Direction.DOWN && elevator.getCurrentFloor() >= request.getTargetFloor())
return true;
}
return false;
}
} ElevatorSystemDemo.java 37 lines GitHub ↗
package com.practice.phonepe.questions.elevatorsystem;
import com.practice.phonepe.questions.elevatorsystem.enums.Direction;
public class ElevatorSystemDemo {
public static void main(String[] args) throws InterruptedException {
// Setup: A building with 2 elevators
int numElevators = 2;
// The getInstance method now initializes the elevators and attaches the Display (Observer).
ElevatorSystem elevatorSystem = ElevatorSystem.getInstance(numElevators);
// Start the elevator system
elevatorSystem.start();
System.out.println("Elevator system started. ConsoleDisplay is observing.\n");
// --- SIMULATION START ---
// 1. External Request: User at floor 5 wants to go UP.
// The system will dispatch this to the nearest elevator (likely E1 or E2, both at floor 1).
elevatorSystem.requestElevator(5, Direction.UP);
Thread.sleep(100); // Wait for the elevator to start moving
// 2. Internal Request: Assume E1 took the previous request.
// The user gets in at floor 5 and presses 10.
// We send this request directly to E1.
// Note: In a real simulation, we'd wait until E1 reaches floor 5, but for this demo,
// we simulate the internal button press shortly after the external one.
elevatorSystem.selectFloor(1, 10);
Thread.sleep(200);
// 3. External Request: User at floor 3 wants to go DOWN.
// E2 (likely still idle at floor 1) might take this, or E1 if it's convenient.
elevatorSystem.requestElevator(3, Direction.DOWN);
Thread.sleep(300);
// 4. Internal Request: User in E2 presses 1.
elevatorSystem.selectFloor(2, 1);
// Let the simulation run for a while to observe the display updates
System.out.println("\n--- Letting simulation run for 1 second ---");
Thread.sleep(1000);
// Shutdown the system
elevatorSystem.shutdown();
System.out.println("\n--- SIMULATION END ---");
}
}