LLD for Elevator System: Designing a Smart Lift
OOP design of an elevator system: Encapsulation, Abstraction, Polymorphism, SOLID principles, and two design patterns — all in one domain model.
Abstract AlgorithmsTLDR
TLDR: An elevator system is a textbook OOP design exercise:
ElevatorCarencapsulates its stop queue,ElevatorStatepolymorphically handles direction changes (State Pattern), andDispatchStrategykeeps assignment algorithms swappable (Strategy Pattern + OCP). SCAN scheduling minimizes travel distance across all three cars.
📖 The Hotel Lift Problem: What We Are Designing
A hotel has 10 floors and 3 elevators. At any moment:
- External requests come in: "Someone on Floor 5 pressed UP."
- Internal requests come in: "Someone inside Lift 1 pressed Floor 10."
- The system must decide which car to dispatch, and which order to serve floors.
This is a classic State Machine + Scheduling design. The key insight is that a lift behaves differently based on its current state — and we should model that explicitly.
🔍 Elevator System Basics: Entities, States, and Button Types
Before writing a single line of code it helps to name every moving part in plain English.
Core entities:
| Entity | Role |
| Building | Container for floors, elevator cars, and the dispatcher |
| ElevatorCar | A single physical lift cabin; holds position, direction, and a queue of stops |
| Floor | A level of the building; has an UP button and a DOWN button |
| Request | A record of "someone needs a lift"; carries a source floor and a desired direction |
| Dispatcher | The brains — receives every request and assigns it to the most suitable car |
States an ElevatorCar can be in:
IDLE— the car is stopped and has no pending stops.MOVING_UP— the car is travelling upward and serving stops along the way.MOVING_DOWN— the car is travelling downward and serving stops along the way.
Two kinds of button press:
- External request — pressed on a floor panel (e.g., "Floor 5, going UP"). The Dispatcher chooses which car to send.
- Internal request — pressed inside the cabin (e.g., "I want Floor 10"). The car adds the destination directly to its own stop queue.
Understanding this split is important: external requests go through the Dispatcher (so they can be balanced across all cars), while internal requests bypass it entirely.
🔢 Domain Model: The Core Entities
classDiagram
class Building {
+List~Floor~ floors
+List~ElevatorCar~ cars
+Dispatcher dispatcher
+handleExternalRequest(Request r)
}
class ElevatorCar {
+int id
+int currentFloor
+Direction direction
+ElevatorState state
-TreeSet~Integer~ stops
+addStop(int floor)
+move()
+getCurrentFloor() int
}
class Dispatcher {
-DispatchStrategy strategy
+assign(Request r) ElevatorCar
}
class Request {
+int sourceFloor
+Direction direction
}
class ElevatorState {
<>
+handleExternalRequest(ElevatorCar car, Request r)
+handleInternalRequest(ElevatorCar car, int floor)
+isIdle() bool
}
class DispatchStrategy {
<>
+assign(Request request, List~ElevatorCar~ available) ElevatorCar
}
class IdleState {
+handleExternalRequest(ElevatorCar, Request)
+handleInternalRequest(ElevatorCar, int)
+isIdle() bool
}
class MovingUpState {
+handleExternalRequest(ElevatorCar, Request)
+handleInternalRequest(ElevatorCar, int)
+isIdle() bool
}
class MovingDownState {
+handleExternalRequest(ElevatorCar, Request)
+handleInternalRequest(ElevatorCar, int)
+isIdle() bool
}
class NearestCarStrategy {
+assign(Request, List~ElevatorCar~) ElevatorCar
}
Building "1" *-- "many" ElevatorCar : contains
Building --> Dispatcher : uses
Dispatcher o-- DispatchStrategy : uses (injected)
ElevatorCar o-- ElevatorState : state (State Pattern)
ElevatorState <|.. IdleState : implements
ElevatorState <|.. MovingUpState : implements
ElevatorState <|.. MovingDownState : implements
DispatchStrategy <|.. NearestCarStrategy : implements
Dispatcher --> ElevatorCar : assigns
- ElevatorCar: Holds a sorted set of destination floors. Has a
state(IDLE, MOVING_UP, MOVING_DOWN). - Dispatcher: The brain. Assigns incoming
Requestto the best car. - Request: Either
ExternalRequest(floor + direction pressed outside) orInternalRequest(destination floor pressed inside the car).
⚙️ The SCAN Algorithm: Why Not First-Come-First-Served?
FCFS is naive: if requests come from floors 10, 1, 10, the lift zigzags 1→10→1→10 (4 × 9 = 36 floors traveled).
The SCAN Algorithm (also called Elevator Algorithm):
- Move in the current direction (UP) serving all stops along the way.
- When no more stops in that direction, reverse.
Toy example: Lift at Floor 4, moving UP. Requests: Floor 6 (UP), Floor 2 (DOWN), Floor 8 (UP).
| Step | Lift Position | Action |
| 1 | Floor 4 → 6 | Serve Floor 6 (on the way UP) |
| 2 | Floor 6 → 8 | Serve Floor 8 (on the way UP) |
| 3 | Floor 8 (switch) | No more UP requests → reverse to DOWN |
| 4 | Floor 8 → 2 | Serve Floor 2 on the way DOWN |
Total travel: 6 floors instead of 18 (FCFS). This is why real elevators use SCAN variants.
🧠 Deep Dive: State Pattern for Elevator Movement
An elevator's behavior depends on its current state. We make this explicit with the State pattern:
public interface ElevatorState {
void handleRequest(ElevatorCar car, int targetFloor);
String name();
}
public class IdleState implements ElevatorState {
public void handleRequest(ElevatorCar car, int targetFloor) {
car.addStop(targetFloor);
if (targetFloor > car.getCurrentFloor()) {
car.setState(new MovingUpState());
} else if (targetFloor < car.getCurrentFloor()) {
car.setState(new MovingDownState());
}
}
public String name() { return "IDLE"; }
}
public class MovingUpState implements ElevatorState {
public void handleRequest(ElevatorCar car, int targetFloor) {
// SCAN: only add if it's above current floor
if (targetFloor >= car.getCurrentFloor()) {
car.addStop(targetFloor); // serve on the way
} else {
car.deferRequest(targetFloor); // serve on the next sweep
}
}
public String name() { return "MOVING_UP"; }
}
The state transitions:
stateDiagram-v2
[*] --> IDLE
IDLE --> MOVING_UP : request above current floor
IDLE --> MOVING_DOWN : request below current floor
MOVING_UP --> IDLE : no more up requests
MOVING_UP --> MOVING_DOWN : switch at top
MOVING_DOWN --> IDLE : no more down requests
MOVING_DOWN --> MOVING_UP : switch at bottom
⚙️ Dispatcher: Strategy Pattern for Assignment Policy
The Dispatcher uses the Strategy pattern so we can swap assignment algorithms:
public interface DispatchStrategy {
ElevatorCar select(List<ElevatorCar> cars, Request request);
}
// Nearest-Car strategy: pick the car that will reach the requester soonest
public class NearestCarStrategy implements DispatchStrategy {
public ElevatorCar select(List<ElevatorCar> cars, Request request) {
return cars.stream()
.filter(c -> c.canService(request))
.min(Comparator.comparingInt(c ->
Math.abs(c.getCurrentFloor() - request.getSourceFloor())))
.orElse(null);
}
}
canService() checks the car's state:
- IDLE → always serviceable.
- MOVING_UP → serviceable if request is above current floor and direction matches.
- MOVING_DOWN → serviceable if request is below current floor and direction matches.
📐 Interface Contracts: DispatchStrategy and ElevatorState
Two interfaces form the entire behavioral backbone of this design. Every concrete class — state implementations, dispatch algorithms, the Dispatcher itself — is wired together through these two contracts.
// Who implements: NearestCarStrategy, ZoneStrategy, LookStrategy
// Who consumes: Dispatcher
interface DispatchStrategy {
ElevatorCar assign(Request request, List<ElevatorCar> available);
}
// Who implements: IdleState, MovingUpState, MovingDownState
// Who consumes: ElevatorCar.handleExternalRequest(), ElevatorCar.handleInternalRequest()
interface ElevatorState {
void handleExternalRequest(ElevatorCar car, Request request);
void handleInternalRequest(ElevatorCar car, int targetFloor);
boolean isIdle();
}
| Interface | Abstracts | Implemented by | Consumed by |
DispatchStrategy | Car assignment algorithm | NearestCarStrategy, ZoneStrategy | Dispatcher |
ElevatorState | Per-direction request handling | IdleState, MovingUpState, MovingDownState | ElevatorCar |
Why separate handleExternalRequest from handleInternalRequest?
External requests originate from a floor panel and route through the Dispatcher — the strategy can load-balance across cars. Internal requests (button pressed inside the cabin) skip the Dispatcher entirely and go straight into the car's stop queue. Modelling this split in the interface makes the routing contract explicit: an IdleState that misroutes an external request is a compile-time contract violation, not a silent logic bug discovered at runtime.
🧱 OOP Pillars in the Elevator Design
Each of the four OOP pillars maps cleanly to a specific boundary in this design. The table below names the pillar, the class or interface that embodies it, and the concrete mechanism.
| OOP Pillar | Class / Interface | Mechanism |
| Encapsulation | ElevatorCar | stopQueue (TreeSet<Integer>) is private. Callers call addStop(int floor) — they never manipulate the set directly. currentFloor is read-only externally via getCurrentFloor(). |
| Abstraction | DispatchStrategy | Building calls dispatch(request, cars) without knowing whether the algorithm is SCAN, SSTF, or round-robin. Assignment logic is hidden behind the interface. |
| Inheritance | ElevatorState hierarchy | ElevatorState is an interface; IdleState, MovingUpState, and MovingDownState are concrete implementations that inherit the behavioural contract. |
| Polymorphism | elevator.handleExternalRequest(r) | The same method call on ElevatorCar produces a different result depending on which ElevatorState object is currently held. A car in IdleState transitions and starts moving; a car in MovingUpState defers the request if it is below the current floor. |
Encapsulation deep-dive: The TreeSet<Integer> inside ElevatorCar is the most critical encapsulation boundary in the design. If external code could add floors to the set arbitrarily, it could insert a below-current-floor stop while the car is sweeping up — silently breaking the SCAN invariant. addStop(int floor) is the single gated entry point; the car's state machine always remains internally consistent regardless of how many concurrent requests arrive.
Polymorphism deep-dive: ElevatorCar.handleExternalRequest(request) delegates to this.state.handleExternalRequest(this, request). The Dispatcher never inspects car.getState() before calling the method. It always calls the same interface method, and the currently-assigned ElevatorState object handles the routing decision. This is the State Pattern realising runtime polymorphism — four classes, one call site.
✅ SOLID Principles in the Elevator Design
| Principle | Applied where | How it shows up |
| SRP | ElevatorCar, Dispatcher, ElevatorState | Three distinct responsibilities in three distinct classes: ElevatorCar manages physical movement and stop sequencing; Dispatcher manages car selection; ElevatorState manages per-direction transition logic. None bleeds into another. |
| OCP | DispatchStrategy | Adding a LOOK algorithm or a peak-hour destination-dispatch algorithm means writing a new class that implements DispatchStrategy. Building, Dispatcher, and ElevatorCar are untouched — open for extension, closed for modification. |
| LSP | ElevatorState implementations | IdleState, MovingUpState, and MovingDownState all honour handleExternalRequest() and handleInternalRequest() without surprising the caller. Any state object can replace another in the ElevatorCar.state field without breaking the car's contract. |
| ISP | DispatchStrategy, ElevatorState | Both interfaces are focused and minimal. DispatchStrategy has one method; ElevatorState has three tightly related methods. No concrete implementation is forced to stub out irrelevant methods. |
| DIP | Dispatcher | Dispatcher depends on DispatchStrategy (the abstraction), not on NearestCarStrategy (the concrete class). The strategy is injected at construction — swap the algorithm without touching Dispatcher. |
OCP in practice: Suppose building management needs peak-hour destination dispatch (group passengers heading to the same zone into one car). The only code change is:
public class DestinationDispatchStrategy implements DispatchStrategy {
@Override
public ElevatorCar assign(Request request, List<ElevatorCar> available) {
// group by destination zone, assign the zone-car
return available.stream()
.filter(c -> sameZone(c, request))
.findFirst()
.orElse(nearestIdleCar(available, request));
}
}
Wire it in: new Dispatcher(new DestinationDispatchStrategy()). Building, ElevatorCar, and all state classes are untouched. This is OCP in action — the system absorbs a fundamentally different dispatch algorithm without a single modification to existing code.
📊 Elevator Request Flow: From Button Press to Served Floor
Here is the end-to-end journey of a single external request — someone pressing the UP button on Floor 3.
flowchart TD
A([External Request:\nFloor 3 pressed UP]) --> B[Dispatcher receives request]
B --> C{Any car\nalready heading UP\npast Floor 3?}
C -- Yes --> D[Assign to that car\nSCAN-add Floor 3 as a stop]
C -- No --> E[Pick nearest IDLE car\nor least-loaded car]
E --> D
D --> F[ElevatorCar adds Floor 3\nto its TreeSet of stops]
F --> G{Car state?}
G -- IDLE --> H[Transition to MOVING_UP\nstart moving]
G -- MOVING_UP --> I[Continue current sweep\nFloor 3 served en route]
H --> J[Stop at Floor 3\nopen doors]
I --> J
J --> K[Serve passenger]
K --> L{More stops\nin current direction?}
L -- Yes --> M[Continue moving]
L -- No --> N[Reverse direction\nor transition to IDLE]
What the diagram shows:
- The Dispatcher is the decision gate. It checks whether any car can serve the request on its current sweep before allocating an idle car.
- A car running SCAN never doubles back until it has exhausted all stops in the current direction — that is the key efficiency gain over FCFS.
TreeSet<Integer>keeps stops sorted, so the car always picks the next floor in the right direction in O(log n).
⚖️ Trade-offs & Failure Modes in Elevator Design
| Pattern | Where | Why |
| State | ElevatorCar.state | Behavior changes based on direction; avoids if (IDLE) {...} else if (MOVING_UP) {...} |
| Strategy | Dispatcher | Swap between Nearest-Car, Least-Loaded, and Zone-Based dispatch without changing the Dispatcher class |
| Command | Request objects | Encapsulate a floor request; supports queuing and undo (open-door requests) |
| Observer | Floor display panels | Subscribe to ElevatorCar.currentFloor updates to show position |
🌍 Real-World Applications of Elevator System Design
Elevator LLD is not an academic exercise — every building with more than a handful of floors runs some variant of this architecture.
- Smart commercial buildings (Otis, KONE, Schindler) — modern lift controllers use destination-dispatch: passengers enter their target floor at a kiosk before boarding. The dispatcher groups passengers by destination zone, often running a Strategy variant that minimises total energy consumption rather than simple nearest-car.
- Hospital lifts — beds and emergency trolleys need priority scheduling. A real hospital system adds a
Priorityfield to the Request object and the Dispatcher's Strategy ranks by priority before distance. - Freight and goods lifts — these spend most of their time waiting at loading bays. The State Pattern maps cleanly: a freight car defaults to IDLE at the ground floor between jobs instead of wherever it last stopped.
- Parking garage stackers — multi-level automated car stackers use the same SCAN scheduling logic to sequence vertical platforms.
- Data-centre server lifts — rack-retrieval robots in high-density warehouses (Amazon Robotics, Autostore) apply identical dispatching concepts to horizontal and vertical grid movement.
The common thread: any system with multiple clients competing for shared vertical resources benefits from a SCAN-style sweep scheduler and a Strategy-pluggable dispatcher.
🧪 Practical Exercises: Extend the Elevator System
Try these hands-on extensions to deepen your understanding.
Exercise 1 — Emergency override:
Add an EmergencyState to the elevator state machine. When activated, the car ignores all pending stops, travels directly to Floor 1, and transitions to IDLE. Implement EmergencyState as a new ElevatorState class and wire it into the existing state transitions.
Exercise 2 — Priority requests:
Extend Request with a Priority enum (NORMAL, VIP, EMERGENCY). Modify NearestCarStrategy to always assign EMERGENCY requests to the closest car regardless of direction. Confirm that normal and VIP requests still follow SCAN order.
Exercise 3 — Zone-based dispatch:
In a 20-floor building, split cars into zones: Car A serves floors 1–7, Car B serves 8–14, Car C serves 15–20. Implement a ZoneStrategy that implements DispatchStrategy and routes requests to the zone-car. Add a fallback: if the zone-car is overloaded (>5 pending stops), delegate to the nearest free car in an adjacent zone.
📚 Key Lessons from the Elevator System Design
- Explicit state beats implicit flags. Replacing
if (isMovingUp && !isIdle)withMovingUpState.handleRequest()makes each transition traceable and testable in isolation. - SCAN is the minimal viable scheduler. Before reaching for sophisticated algorithms, verify that a simple directional sweep already handles 95 % of real load patterns efficiently.
- Keep strategy boundaries clean. The Dispatcher should only call
dispatchStrategy.select(). It must never inspect car internals directly — that coupling breaks the Strategy contract and makes swapping algorithms painful. - TreeSet gives you sorted deduplication for free. Storing stops in a
TreeSet<Integer>means the car always has a sorted, duplicate-free list — no extra bookkeeping needed. - Model buttons, not just floors. Separating
ExternalRequest(floor + direction) fromInternalRequest(destination floor only) prevents incorrect routing: a car moving DOWN should not pick up an EXTERNAL UP request mid-sweep.
🧭 Decision Guide: Choosing the Right Elevator Scheduling Strategy
| Condition | Recommendation |
| Elevator behavior changes by direction | State Pattern — avoids nested if-else spaghetti across directions |
| Multiple dispatch strategies needed | Strategy Pattern in Dispatcher — swap Nearest-Car, Least-Loaded, Zone-Based at runtime |
| Standard office or hotel building | SCAN algorithm — handles mixed direction requests with minimal travel |
| High-rise with peak-hour patterns | Destination Dispatch (group similar destination floors) — used in Otis and KONE systems |
| Single elevator, small building | Skip State pattern; simple directional flag + sorted queue suffices |
| Concurrent requests from all floors | TreeSet<Integer> for stops — sorted, deduplicated, O(log N) insert |
Start with the SCAN algorithm and Nearest-Car dispatch. They handle the vast majority of real-world building patterns efficiently. Add zone-based dispatch or destination dispatch only when peak-hour analysis shows SCAN producing unacceptably long wait times on specific floors.
📌 TLDR: Summary & Key Takeaways
- Encapsulation:
ElevatorCarhides itsTreeSet<Integer>stop queue — all access goes throughaddStop(int floor), preserving the SCAN sweep invariant. - Polymorphism via State Pattern:
elevator.handleExternalRequest(r)delegates to the currentElevatorStateobject — the same call produces different behavior inIdleState,MovingUpState, andMovingDownState. - Abstraction via Strategy Pattern:
DispatchercallsdispatchStrategy.assign(request, cars)without knowing whether it is nearest-car, zone-based, or destination dispatch. - SOLID OCP: Adding a new dispatch algorithm is a one-class addition implementing
DispatchStrategy— no existing class changes. - SOLID DIP:
Dispatcheris injected with aDispatchStrategyabstraction — decoupled from any concrete algorithm at construction time.
📝 Practice Quiz
Why is FCFS (First Come First Served) a poor dispatch strategy for elevators?
- A) It requires more memory.
- B) It causes zigzag travel that maximizes floors traveled; SCAN sweeps in one direction, minimizing waste.
- C) FCFS can't handle requests from multiple floors.
Correct Answer: B — FCFS lets the car reverse direction for every new request, multiplying total floors travelled. SCAN sweeps in one direction until no more stops remain, dramatically cutting wasted movement.
In the State Pattern for elevator movement, what happens when a request arrives for a floor below the current floor while the car is MOVING_UP?
- A) The car immediately reverses to MOVING_DOWN.
- B) The request is deferred to the next downward sweep — the car continues upward first.
- C) The request is rejected.
Correct Answer: B — The State Pattern'sMovingUpState.handleRequest()defers below-current-floor requests so the car finishes its upward sweep before reversing. Immediately reversing would break SCAN and penalise passengers already waiting above.
What is the advantage of using a
TreeSet<Integer>to store pending stops in anElevatorCar?- A) It prevents duplicates and keeps stops automatically sorted by floor number, enabling in-order serving.
- B) It uses less memory than a list.
- C) It allows thread-safe concurrent modification without locks.
Correct Answer: A —TreeSetmaintains a sorted, duplicate-free set of floor numbers, so the car always retrieves the next logical stop in O(log n) without any manual sort step.
🛠️ Spring Boot in Action: Exposing the Elevator Controller as a REST API
Spring Boot provides the auto-configured web layer and IoC container that turns the ElevatorController domain model into a live HTTP service. Annotating the dispatcher and elevator cars as @Service beans wires them into Spring's singleton lifecycle — one controller instance coordinates all cars for the lifetime of the application.
How it solves the problem in this post: The domain model — ElevatorCar, Direction, Dispatcher — stays pure Java. Spring wraps it with @RestController, exposing external floor-button presses and internal cabin-button presses as REST endpoints. A @Scheduled background task drives the elevator tick (move one floor, pick up passengers) without blocking request threads.
// ─── Domain enums ────────────────────────────────────────────────────────────
public enum Direction { UP, DOWN, IDLE }
// ─── ElevatorCar: one physical lift cabin ────────────────────────────────────
public class ElevatorCar {
private final int id;
private int currentFloor = 0;
private Direction direction = Direction.IDLE;
private final java.util.TreeSet<Integer> stops = new java.util.TreeSet<>();
public ElevatorCar(int id) { this.id = id; }
public synchronized void addStop(int floor) { stops.add(floor); }
/** Move one floor toward the next stop (called on each scheduler tick) */
public synchronized void tick() {
if (stops.isEmpty()) { direction = Direction.IDLE; return; }
int next = direction == Direction.DOWN ? stops.last() : stops.first();
if (currentFloor < next) { currentFloor++; direction = Direction.UP; }
else if (currentFloor > next) { currentFloor--; direction = Direction.DOWN; }
else { stops.remove(currentFloor); } // arrived — serve the stop
}
public int getCurrentFloor() { return currentFloor; }
public Direction getDirection() { return direction; }
public int getId() { return id; }
}
// ─── Spring service: manages all cars ────────────────────────────────────────
@org.springframework.stereotype.Service
public class ElevatorControllerService {
private final List<ElevatorCar> cars = List.of(
new ElevatorCar(1), new ElevatorCar(2), new ElevatorCar(3));
/** External request: floor button pressed */
public void requestFromFloor(int floor, Direction direction) {
// Nearest-car strategy: pick car closest to requested floor
cars.stream()
.min(java.util.Comparator.comparingInt(c -> Math.abs(c.getCurrentFloor() - floor)))
.ifPresent(car -> car.addStop(floor));
}
/** Internal request: passenger pressed a floor button inside car */
public void requestInsideCar(int carId, int floor) {
cars.stream().filter(c -> c.getId() == carId)
.findFirst().ifPresent(c -> c.addStop(floor));
}
/** Scheduler tick: advance all cars one step */
@org.springframework.scheduling.annotation.Scheduled(fixedRate = 500)
public void tick() { cars.forEach(ElevatorCar::tick); }
public List<ElevatorCar> getCars() { return cars; }
}
// ─── REST controller ─────────────────────────────────────────────────────────
@org.springframework.web.bind.annotation.RestController
@org.springframework.web.bind.annotation.RequestMapping("/api/elevator")
public class ElevatorRestController {
private final ElevatorControllerService svc;
public ElevatorRestController(ElevatorControllerService svc) { this.svc = svc; }
/** POST /api/elevator/call?floor=5&direction=UP */
@org.springframework.web.bind.annotation.PostMapping("/call")
public org.springframework.http.ResponseEntity<String> call(
@org.springframework.web.bind.annotation.RequestParam int floor,
@org.springframework.web.bind.annotation.RequestParam Direction direction) {
svc.requestFromFloor(floor, direction);
return org.springframework.http.ResponseEntity.accepted()
.body("Request queued for floor " + floor + " going " + direction);
}
/** POST /api/elevator/select?carId=1&floor=9 */
@org.springframework.web.bind.annotation.PostMapping("/select")
public org.springframework.http.ResponseEntity<String> select(
@org.springframework.web.bind.annotation.RequestParam int carId,
@org.springframework.web.bind.annotation.RequestParam int floor) {
svc.requestInsideCar(carId, floor);
return org.springframework.http.ResponseEntity.accepted()
.body("Car " + carId + " will stop at floor " + floor);
}
/** GET /api/elevator/status */
@org.springframework.web.bind.annotation.GetMapping("/status")
public org.springframework.http.ResponseEntity<List<Map<String, Object>>> status() {
var result = svc.getCars().stream()
.map(c -> Map.<String, Object>of(
"id", c.getId(),
"floor", c.getCurrentFloor(),
"direction", c.getDirection()))
.toList();
return org.springframework.http.ResponseEntity.ok(result);
}
}
@Scheduled(fixedRate = 500) fires every 500 ms — each tick moves every car one floor toward its next stop, simulating the elevator's physical travel. The TreeSet<Integer> inside each ElevatorCar keeps stops sorted, implementing the SCAN algorithm naturally: stops.first() is the lowest floor when going up, stops.last() is the highest when going down.
For a full deep-dive on Spring Boot scheduling and stateful elevator simulation, a dedicated follow-up post is planned.
🔗 Related Posts
- LLD for Parking Lot System — another classic OOP design with State and Strategy pattern parallels.
- LLD for Movie Booking System — multi-seat concurrency and request scheduling in a real-time booking context.
- LLD for URL Shortener — single-responsibility decomposition applied to a high-traffic write-heavy service.
- LLD for LRU Cache: Designing a High-Performance Cache — data-structure-driven LLD showing how TreeMap and LinkedHashMap power cache eviction.
- Exploring the Strategy Design Pattern — deep dive into the exact pattern used for
DispatchStrategyin this post. - Single Responsibility Principle — the foundational SOLID rule that keeps Building, ElevatorCar, and Dispatcher cleanly separated.

Written by
Abstract Algorithms
@abstractalgorithms
More Posts
Types of LLM Quantization: By Timing, Scope, and Mapping
TLDR: There is no single "best" LLM quantization. You classify and choose quantization along three axes: when you quantize (timing), what you quantize (scope), and how values are encoded (mapping). In practice, most teams start with weight quantizati...
Stream Processing Pipeline Pattern: Stateful Real-Time Data Products
TLDR: Stream pipelines succeed when event-time semantics, state management, and replay strategy are designed together — and Kafka Streams lets you build all three directly inside your Spring Boot service. Stripe's real-time fraud detection processes...
Service Mesh Pattern: Control Plane, Data Plane, and Zero-Trust Traffic
TLDR: A service mesh intercepts all service-to-service traffic via injected Envoy sidecar proxies, letting a platform team enforce mTLS, retries, timeouts, and circuit breaking centrally — without changing application code. Reach for it when cross-te...
Serverless Architecture Pattern: Event-Driven Scale with Operational Guardrails
TLDR: Serverless is strongest for spiky asynchronous workloads when cold-start, observability, and state boundaries are intentionally designed. TLDR: Serverless works best for spiky, event-driven workloads when you design for idempotency, observabili...
