Loading learning content...
With a solid understanding of requirements, we now transition to domain modeling—the process of identifying the core entities that will form the backbone of our elevator system. This is where abstract requirements become concrete classes.
Entity identification is both an art and a science. We apply systematic techniques like noun extraction while also leveraging domain expertise to distinguish true entities from mere attributes or implementation details. The goal is to create a model that is:
By the end of this page, you will be able to:
• Apply noun extraction to identify candidate entities from requirements • Distinguish between entities, value objects, and services • Define comprehensive attributes and behaviors for each entity • Establish clear relationships and multiplicities between entities • Create a domain model ready for behavioral design
The noun extraction technique is a systematic approach to identifying candidate entities. We scan our requirements documentation, extracting every noun and noun phrase, then filter and refine this list.
Step 1: Extract All Nouns from Requirements
From our requirements analysis, let's extract key nouns:
| Source Requirement | Extracted Nouns |
|---|---|
| FR1: Floor Request (Hall Call) | floor, passenger, elevator, button, request, system |
| FR2: Destination Request (Cab Call) | passenger, elevator, destination, floor |
| FR3: Door Operations | elevator, door, floor, duration |
| FR4: Multi-Elevator Coordination | building, elevator, request, dispatch |
| FR5: Direction Indication | passenger, direction, elevator |
| FR6: Floor Display | passenger, elevator, floor, direction, display |
| FR7: Emergency Stop | passenger, emergency, elevator |
| FR8: Maintenance Mode | administrator, elevator, service, maintenance |
Step 2: Consolidate and Categorize
After removing duplicates and grouping related terms:
Physical Entities: elevator, floor, building, door, button, display Actors: passenger, administrator, system Concepts: request, direction, destination, duration, maintenance, emergency Actions (masquerading as nouns): dispatch, service
Step 3: Filter Non-Entities
Not every noun becomes an entity. We apply these filters:
Experienced engineers develop intuition for entity identification. When you see 'request,' you immediately think: 'This has creation time, source floor, direction, and state. It tracks through the system. It's definitely an entity.' This intuition comes from practice—treat this case study as practice.
The Elevator is the central entity in our system—the physical device that carries passengers between floors. It has rich state, complex behavior, and serves as the focal point for most system operations.
Entity Analysis:
Detailed Attribute Definition:
| Attribute | Type | Description | Initial Value |
|---|---|---|---|
| id | ElevatorId (int/string) | Unique identifier | Assigned at creation |
| currentFloor | Floor | The floor where elevator is currently located | Ground floor (0) |
| direction | Direction enum | Current travel direction (UP, DOWN, IDLE) | IDLE |
| doorState | DoorState enum | Door status (OPEN, OPENING, CLOSED, CLOSING) | CLOSED |
| state | ElevatorState enum | Operational state (MOVING, STOPPED, MAINTENANCE) | STOPPED |
| destinationQueue | List<Request> | Ordered list of floors to visit | Empty |
| capacity | int | Maximum passenger capacity | 10 |
| currentLoad | int | Current passenger count (if weight sensors) | 0 |
Behavioral Responsibilities:
The Elevator entity must expose methods for all operations it can perform:
12345678910111213141516171819202122232425262728293031
interface IElevator { // Identity readonly id: ElevatorId; // State Queries getCurrentFloor(): Floor; getDirection(): Direction; getDoorState(): DoorState; getState(): ElevatorState; isAtCapacity(): boolean; // State Commands moveToFloor(floor: Floor): void; openDoors(): void; closeDoors(): void; stop(): void; // Request Management addDestination(request: Request): void; removeDestination(floor: Floor): void; getNextDestination(): Floor | null; hasDestination(floor: Floor): boolean; // Lifecycle enterMaintenanceMode(): void; exitMaintenanceMode(): void; // Observer Pattern Support registerObserver(observer: ElevatorObserver): void; notifyObservers(event: ElevatorEvent): void;}Notice that Direction, DoorState, and ElevatorState are enums, not entities. They have no identity—'UP' is always 'UP'. They are value objects that describe aspects of the Elevator entity. This distinction matters for design purity.
The Floor entity represents a physical floor in the building—the location where passengers wait for elevators and where elevators stop. Some designs treat floors as simple integers, but a richer Floor entity enables future extensions.
Entity Justification:
Design Decision: Floor as Entity vs Integer
Our Choice: We'll model Floor as a lightweight entity that encapsulates floor number and associated behavior. This provides extensibility without excessive complexity.
Floor Entity Definition:
| Attribute | Type | Description |
|---|---|---|
| floorNumber | int | The floor level (0 = ground, 1, 2, ...) |
| name | string | Human-readable name ('Lobby', 'Parking', 'Penthouse') |
| upButtonActive | boolean | Is the UP hall call button lit/pending? |
| downButtonActive | boolean | Is the DOWN hall call button lit/pending? |
| accessRestricted | boolean | Does this floor require special access? |
| isServiceFloor | boolean | Is this a maintenance/service floor? |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
class Floor { private readonly floorNumber: number; private readonly name: string; private upButtonActive: boolean = false; private downButtonActive: boolean = false; private accessRestricted: boolean = false; constructor(floorNumber: number, name?: string) { this.floorNumber = floorNumber; this.name = name ?? `Floor ${floorNumber}`; } // Identity getFloorNumber(): number { return this.floorNumber; } getName(): string { return this.name; } // Hall call management requestUp(): void { this.upButtonActive = true; // Notify controller of hall call } requestDown(): void { this.downButtonActive = true; // Notify controller of hall call } satisfyUpRequest(): void { this.upButtonActive = false; } satisfyDownRequest(): void { this.downButtonActive = false; } hasActiveRequest(direction: Direction): boolean { return direction === Direction.UP ? this.upButtonActive : this.downButtonActive; } // Floor comparison (for collection operations) equals(other: Floor): boolean { return this.floorNumber === other.floorNumber; } isAbove(other: Floor): boolean { return this.floorNumber > other.floorNumber; } isBelow(other: Floor): boolean { return this.floorNumber < other.floorNumber; }}The Request entity represents a passenger's desire to use the elevator system. This is a crucial entity that tracks the lifecycle of an elevator call from initiation to satisfaction.
Request Types:
There are two fundamentally different types of requests:
These differ in their attributes and lifecycle, warranting either two classes or a unified class with type discrimination.
| Aspect | Hall Call | Cab Call |
|---|---|---|
| Source | Floor button panel | Inside elevator panel |
| Attributes | Floor + Direction | Floor only (direction implied) |
| Assigned To | Best available elevator | Current elevator |
| Satisfies When | Elevator arrives going correct direction | Elevator reaches floor |
| Can Be Shared | Multiple passengers same floor/direction | Specific to one elevator |
Design Decision: Unified vs Separate Request Classes
We'll use a unified Request class with type discrimination. This simplifies collection handling while allowing type-specific logic where needed.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
enum RequestType { HALL_CALL = 'HALL_CALL', CAB_CALL = 'CAB_CALL'} enum RequestStatus { PENDING = 'PENDING', ASSIGNED = 'ASSIGNED', IN_PROGRESS = 'IN_PROGRESS', SATISFIED = 'SATISFIED', CANCELLED = 'CANCELLED'} class Request { private static nextId = 1; readonly id: number; readonly type: RequestType; readonly floor: Floor; readonly direction: Direction | null; // null for cab calls readonly createdAt: Date; private status: RequestStatus; private assignedElevator: Elevator | null = null; private satisfiedAt: Date | null = null; constructor( type: RequestType, floor: Floor, direction: Direction | null = null ) { this.id = Request.nextId++; this.type = type; this.floor = floor; this.direction = direction; this.createdAt = new Date(); this.status = RequestStatus.PENDING; // Validate: Hall calls require direction if (type === RequestType.HALL_CALL && direction === null) { throw new Error('Hall calls must specify direction'); } } // Factory methods for clarity static createHallCall(floor: Floor, direction: Direction): Request { return new Request(RequestType.HALL_CALL, floor, direction); } static createCabCall(floor: Floor): Request { return new Request(RequestType.CAB_CALL, floor); } // Status management assign(elevator: Elevator): void { if (this.status !== RequestStatus.PENDING) { throw new Error(`Cannot assign request in status ${this.status}`); } this.assignedElevator = elevator; this.status = RequestStatus.ASSIGNED; } beginService(): void { this.status = RequestStatus.IN_PROGRESS; } satisfy(): void { this.status = RequestStatus.SATISFIED; this.satisfiedAt = new Date(); } cancel(): void { this.status = RequestStatus.CANCELLED; } // Queries getWaitTime(): number { const endTime = this.satisfiedAt ?? new Date(); return endTime.getTime() - this.createdAt.getTime(); } isPending(): boolean { return this.status === RequestStatus.PENDING; } isSatisfied(): boolean { return this.status === RequestStatus.SATISFIED; } isHallCall(): boolean { return this.type === RequestType.HALL_CALL; } isCabCall(): boolean { return this.type === RequestType.CAB_CALL; }}The Request entity's status transitions form a mini state machine: PENDING → ASSIGNED → IN_PROGRESS → SATISFIED. Understanding and modeling this lifecycle prevents bugs like double-assignment or satisfaction of cancelled requests.
The Controller (or ElevatorController) is the brain of the system—the coordinaing entity that receives requests, dispatches elevators, and ensures optimal system behavior. In pattern terms, it's a Mediator that decouples elevators from request sources.
Controller Responsibilities:
Is Controller an Entity or a Service?
Technically, Controller is closer to a Domain Service than an Entity—it has no meaningful identity (there's only one controller) and exists primarily to orchestrate other entities. However, modeling it as a class with state (list of elevators, pending requests) works well.
Controller Design:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
interface SchedulingStrategy { selectElevator( request: Request, elevators: Elevator[] ): Elevator | null;} class ElevatorController { private elevators: Elevator[] = []; private floors: Floor[] = []; private pendingHallCalls: Request[] = []; private schedulingStrategy: SchedulingStrategy; private observers: ControllerObserver[] = []; constructor( numFloors: number, numElevators: number, strategy: SchedulingStrategy ) { // Initialize floors for (let i = 0; i < numFloors; i++) { this.floors.push(new Floor(i)); } // Initialize elevators for (let i = 0; i < numElevators; i++) { const elevator = new Elevator(i, this.floors[0]); elevator.registerObserver(this.createElevatorObserver()); this.elevators.push(elevator); } this.schedulingStrategy = strategy; } // Request handling handleHallCall(floorNumber: number, direction: Direction): void { const floor = this.getFloor(floorNumber); // Avoid duplicate requests if (this.hasPendingRequest(floor, direction)) { return; } const request = Request.createHallCall(floor, direction); // Try to dispatch immediately const elevator = this.schedulingStrategy.selectElevator( request, this.getAvailableElevators() ); if (elevator) { request.assign(elevator); elevator.addDestination(request); this.notifyObservers('request-assigned', { request, elevator }); } else { // Queue for later dispatch this.pendingHallCalls.push(request); this.notifyObservers('request-queued', { request }); } // Activate floor button indicator if (direction === Direction.UP) { floor.requestUp(); } else { floor.requestDown(); } } handleCabCall(elevatorId: number, floorNumber: number): void { const elevator = this.getElevator(elevatorId); const floor = this.getFloor(floorNumber); // Cab calls go directly to the specific elevator const request = Request.createCabCall(floor); request.assign(elevator); elevator.addDestination(request); this.notifyObservers('cab-call-registered', { elevator, floor }); } // Elevator events (Observer pattern) onElevatorArrived(elevator: Elevator, floor: Floor): void { // Satisfy matching requests this.satisfyRequests(elevator, floor); // Try to assign queued requests this.processQueuedRequests(); } // Strategy pattern for algorithm swapping setSchedulingStrategy(strategy: SchedulingStrategy): void { this.schedulingStrategy = strategy; } // Queries getElevatorStatuses(): ElevatorStatus[] { return this.elevators.map(e => ({ id: e.id, floor: e.getCurrentFloor().getFloorNumber(), direction: e.getDirection(), state: e.getState(), destinationCount: e.getDestinationQueue().length })); } private getAvailableElevators(): Elevator[] { return this.elevators.filter(e => e.getState() !== ElevatorState.MAINTENANCE ); } private hasPendingRequest(floor: Floor, direction: Direction): boolean { return this.pendingHallCalls.some(r => r.floor.equals(floor) && r.direction === direction && r.isPending() ); } private satisfyRequests(elevator: Elevator, floor: Floor): void { // Mark matching requests as satisfied const direction = elevator.getDirection(); // Satisfy hall calls this.pendingHallCalls .filter(r => r.floor.equals(floor) && r.direction === direction) .forEach(r => r.satisfy()); // Deactivate floor button if (direction === Direction.UP) { floor.satisfyUpRequest(); } else { floor.satisfyDownRequest(); } } private processQueuedRequests(): void { const unassigned = this.pendingHallCalls.filter(r => r.isPending()); for (const request of unassigned) { const elevator = this.schedulingStrategy.selectElevator( request, this.getAvailableElevators() ); if (elevator) { request.assign(elevator); elevator.addDestination(request); } } }}The Controller can easily become a 'God Class' that does everything. To prevent this, delegate scheduling logic to a Strategy, state management to the Elevator's State pattern, and event handling to Observers. The Controller should coordinate, not compute.
Beyond the primary entities, our domain requires several value objects and enumerations that provide semantic richness without the overhead of full entities.
Value Object vs Entity:
Core Enumerations:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
// Direction of travelenum Direction { UP = 'UP', DOWN = 'DOWN', IDLE = 'IDLE' // Not moving} // Elevator door statesenum DoorState { OPEN = 'OPEN', OPENING = 'OPENING', CLOSED = 'CLOSED', CLOSING = 'CLOSING'} // Overall elevator operational stateenum ElevatorState { IDLE = 'IDLE', // Stationary, no pending requests MOVING = 'MOVING', // Traveling between floors STOPPED = 'STOPPED', // At a floor, doors operating MAINTENANCE = 'MAINTENANCE' // Out of service} // Useful type aliases for claritytype ElevatorId = number;type FloorNumber = number;type Timestamp = Date; // Value object for distance calculationsclass FloorDistance { readonly from: FloorNumber; readonly to: FloorNumber; readonly distance: number; readonly direction: Direction; constructor(from: FloorNumber, to: FloorNumber) { this.from = from; this.to = to; this.distance = Math.abs(to - from); this.direction = to > from ? Direction.UP : to < from ? Direction.DOWN : Direction.IDLE; } // Value object equality based on attributes equals(other: FloorDistance): boolean { return this.from === other.from && this.to === other.to; }} // Configuration value objectclass BuildingConfig { readonly numFloors: number; readonly numElevators: number; readonly floorHeight: number; // meters readonly elevatorSpeed: number; // floors per second readonly doorOpenDuration: number; // milliseconds constructor(config: Partial<BuildingConfig> = {}) { this.numFloors = config.numFloors ?? 20; this.numElevators = config.numElevators ?? 4; this.floorHeight = config.floorHeight ?? 3.5; this.elevatorSpeed = config.elevatorSpeed ?? 0.5; this.doorOpenDuration = config.doorOpenDuration ?? 3000; } validate(): boolean { return this.numFloors > 1 && this.numElevators > 0 && this.elevatorSpeed > 0; }}Value objects should ideally be immutable. In TypeScript, use 'readonly'; in Python, use dataclass(frozen=True). This prevents bugs from shared mutable state and enables safe caching.
With entities defined, we now establish the relationships between them. This determines how entities reference each other and forms the basis for our class diagram.
Relationship Types:
| Relationship | Type | Cardinality | Description |
|---|---|---|---|
| Controller → Elevator | Aggregation | 1 : N | Controller manages multiple elevators |
| Controller → Floor | Aggregation | 1 : N | Controller knows all floors |
| Controller → Request | Association | 1 : N | Controller tracks pending hall calls |
| Elevator → Floor | Association | N : 1 | Elevator is at one floor at a time |
| Elevator → Request | Aggregation | 1 : N | Elevator has destination queue |
| Request → Floor | Association | N : 1 | Request targets a specific floor |
| Request → Elevator | Association | N : 0..1 | Request may be assigned to an elevator |
| Floor → Request | Dependency | Floor creates requests but doesn't track them |
Visual Representation:
In the diagram, Controller→Elevator is clear, but should Elevator→Controller exist? Generally no—it creates circular dependencies. Instead, use the Observer pattern: Elevator notifies interested parties (including Controller) of events without knowing who's listening.
Entity Modeling Complete:
We've established a comprehensive domain model with four primary entities and their supporting types:
What's Next:
With static structure defined, we now layer dynamic behavior. The next page covers:
The combination of entity structure (this page) and behavioral patterns (next page) forms a complete, production-ready elevator system design.
You now have a clear picture of the domain: what entities exist, what attributes they carry, and how they relate. This is the foundation upon which state machines, algorithms, and patterns will be built. Keep this mental model as we proceed to behavioral design.