Design a Parking Lot System in JS

Build a full HLD + LLD parking lot system using 5 design patterns in JS.

Introduction

30 Second Summary

Every time you pull into a parking garage, there is a system deciding which spot to send you to, how much to charge you, and how to handle the ten other cars that arrived at the same time. It is a deceptively complex coordination problem hiding behind a simple barrier gate.

In this project, you will design and build a multi-level parking lot management system from scratch. You will produce a complete High Level Design document and then implement the entire Low Level Design in JavaScript, applying five industry-standard design patterns along the way.

What You'll Build

A production-quality system design artifact and working JavaScript implementation that demonstrates senior-level architectural thinking for a parking lot system.

By the end of this project, you'll have:

  • A complete High Level Design document covering system architecture, REST API contracts, database schema, and capacity estimation for a multi-level parking lot.
  • A fully implemented Low Level Design in JavaScript demonstrating five design patterns: Strategy, Factory, Singleton, Observer, and Facade.
  • A working extensibility proof showing how to add EV charging spots and surge pricing without modifying any existing class.
  • Secret Mission: Add a reservation system with time-slot booking and automatic expiry.

Are there any prerequisites?

This project assumes strong OOP knowledge and JavaScript/Node.js proficiency. You should be comfortable with classes, inheritance, and polymorphism before starting.

Set Up Your Project

Before you can design a parking lot system, you need a workspace that mirrors the structure of your final deliverables. Your project will have two sides: design documentation (the HLD artifact) and working implementation code (the LLD).

In this step, you will create a project directory with a clean separation between docs and source code. You will also configure Node.js to use ES modules, which provide modern import/export syntax for your class hierarchy.

In this step, get ready to:
  • Create the project directory and initialize it with npm.
  • Set up the folder structure for design docs and source code.
  • Verify everything works by running a test script.
Create and initialize the project directory

Every coding project starts with a dedicated directory. You will create a folder on your Desktop so it is easy to find.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir parking-lot-system
cd parking-lot-system
  • Initialize the project with npm by running:
npm init -y

What does this command do?

The npm init -y command generates a default package.json file that tracks your project's metadata and dependencies. The -y flag accepts all defaults so you skip the interactive prompts.

  • Confirm the file was created by running:
ls

You should see package.json listed in the output.

Configure the project and create the folder structure

The default package.json does not include ES module support. You need to replace its contents and create the subdirectories that will hold your design docs and source code.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the parking-lot-system folder on your Desktop, then click Open.
  • In the Explorer sidebar, click on package.json to open it.
  • Select all the existing content and replace it with the following:
{
  "name": "parking-lot-system",
  "version": "1.0.0",
  "description": "Parking Lot System - HLD and LLD Implementation",
  "main": "src/main.js",
  "type": "module",
  "scripts": {
    "start": "node src/main.js"
  }
}

What does this configuration do?

Setting "type": "module" tells Node.js to treat all .js files as ES modules. This enables import/export syntax instead of require().

The "main" field points to src/main.js as the entry point. The start script lets you run the project with npm start later.

  • Save package.json.
  • Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, below Esc).
  • Create the folder structure by running this command:
mkdir -p docs src/models src/services src/patterns

What are these folders for?

  • docs/ holds your High Level Design documentation.
  • src/models/ holds domain entities (Vehicle, ParkingSpot, Ticket).
  • src/services/ holds business logic (ParkingFloor, ParkingLot).
  • src/patterns/ holds design pattern implementations (Strategy, Factory, Observer).
  • Create the entry point file with a test message by running:
echo "console.log('Parking Lot System - setup verified');" > src/main.js
  • Confirm the folder structure was created by running:
ls

You should see docs, node_modules, package.json, package-lock.json, and src in the output.

Don't see the folders?

Make sure your terminal is in the parking-lot-system directory. Run pwd to check your current location.

If you are in a different directory, run cd ~/Desktop/parking-lot-system to get back.

help me get into my parking-lot-system project directory

Verify the setup
  • Run the entry point script by running this command:
node src/main.js

You should see Parking Lot System - setup verified printed in your terminal.

Seeing a module error?

If you see an error about ES modules or unexpected token, double check that your package.json includes "type": "module". Without this field, Node.js defaults to CommonJS mode.

help me fix an ES module error in my Node.js project

✔️ Awesome, I've got everything!

Great. Double check you have saved package.json and confirmed the output in your terminal.

ⓧ I'd like to double check the full code

Here is what your package.json should look like:

{
  "name": "parking-lot-system",
  "version": "1.0.0",
  "description": "Parking Lot System - HLD and LLD Implementation",
  "main": "src/main.js",
  "type": "module",
  "scripts": {
    "start": "node src/main.js"
  }
}

And your src/main.js should contain:

console.log('Parking Lot System - setup verified');

Your workspace is ready. Next up, you will define the requirements for the parking lot system, covering functional needs, non-functional constraints, and capacity estimation.

Define Requirements and Capacity

Your project structure is ready. Before writing any code, you need a clear picture of what the system does, how it behaves under pressure, and how big it needs to be.

In this step, you will create a requirements specification that captures the functional behavior, non-functional constraints, and capacity math for a multi-level parking lot. This document becomes the contract that drives every design decision in later steps.

In this step, get ready to:
  • Define functional requirements covering vehicle types, spot types, and entry/exit flows.
  • Define non-functional requirements for latency, concurrency, and scalability.
  • Perform capacity estimation with storage calculations, peak TPS, and algorithm tradeoff analysis.
Define functional requirements

Functional requirements describe what the system does from the user's perspective. For a parking lot, that means defining the vehicle types it accepts, the spot types it offers, and the rules for how vehicles flow in and out.

  • Create a new file called requirements.md inside your docs/ folder.
  • Add the following content to define the document heading and functional requirements section:
# Parking Lot System - Requirements Specification

## Functional Requirements

### FR-1: Multi-Level Structure
- The system manages a parking lot with multiple floors.
- Each floor contains a configurable number of parking spots.

### FR-2: Vehicle Types
- The system supports four vehicle categories: Motorcycle, Car, Truck, and Electric.
- Each vehicle is identified by a unique license plate.

### FR-3: Spot Types and Compatibility
- Four spot types exist: Motorcycle, Compact, Large, and EV Charging.
- Compatibility rules:
  - Motorcycle: fits Motorcycle, Compact, or Large spots.
  - Car: fits Compact or Large spots.
  - Truck: fits only Large spots.
  - Electric: fits only EV Charging spots.

### FR-4: Entry Flow
- A vehicle arrives at an entry gate.
- The system assigns the smallest compatible spot (preference order: exact fit first).
- A ticket is issued containing the vehicle, assigned spot, and entry timestamp.

### FR-5: Exit Flow
- On exit, the system calculates the fee based on duration and vehicle type.
- The spot is released back to the available pool.
- The ticket is marked as paid with the calculated amount.

### FR-6: Real-Time Availability
- The system tracks available spots per type per floor in real time.
- Display boards at entrances show current availability.

### FR-7: Multiple Gates
- Multiple entry and exit gates operate concurrently.
- Two gates must never assign the same spot to different vehicles.

What does this section capture?

Each functional requirement (FR) maps to a distinct user-facing behavior. FR-3 is the most critical for your implementation because the compatibility rules directly drive your class hierarchy later.

The "smallest compatible spot" preference in FR-4 means a motorcycle parks in a motorcycle spot first, not a large spot. This prevents small vehicles from consuming spots that larger vehicles need.

Why include Electric and EV Charging from the start?

You will implement the base system first (Motorcycle, Car, Truck) and add Electric vehicle support later as an extensibility proof. Defining it in requirements now ensures your architecture accounts for it without over-engineering the initial code.

  • Save docs/requirements.md.
  • Open VS Code's Markdown preview by pressing Cmd+Shift+V (macOS) or Ctrl+Shift+V (Windows). You should see a formatted document with numbered requirements and a nested compatibility list under FR-3.

Don't see the Markdown preview?

Make sure your file has the .md extension. VS Code only recognizes Markdown preview for files ending in .md or .markdown.

help me open a Markdown preview in VS Code

Define non-functional requirements

Non-functional requirements describe how the system behaves under stress. These constraints shape your concurrency strategy and database design in later steps.

  • Add the following section below your functional requirements in docs/requirements.md:

## Non-Functional Requirements

### NFR-1: Low Latency
- Gate operations (entry/exit) must complete in sub-100ms.
- Spot lookup and assignment must be optimized for speed.

### NFR-2: Thread Safety
- Concurrent access from multiple gates must be handled safely.
- Optimistic locking on spot assignment prevents double-allocation.

### NFR-3: Horizontal Scalability
- The system shards by lot_id for multi-facility deployments.
- Each lot operates independently with its own state.

### NFR-4: Fault Tolerance
- Edge gate controllers can operate offline with local state.
- Sync occurs when connectivity is restored.

### NFR-5: ACID Compliance
- All financial transactions (fee calculation, payment processing) are ACID-compliant.
- No partial ticket closures or lost payments.

Why do these constraints matter for your code?

NFR-2 (thread safety) drives your optimistic locking implementation in Step 6. When two gates try to assign the same spot at the same instant, only one succeeds. The other retries with the next available spot.

NFR-3 (sharding by lot_id) means your Singleton pattern applies per lot, not globally. Each parking lot instance manages its own floors and spots independently.

  • Save docs/requirements.md.
  • Check the Markdown preview. You should now see two main sections: Functional Requirements and Non-Functional Requirements.
Perform capacity estimation

Capacity estimation answers: how big is this system, and does the architecture need to be clever or simple? For a parking lot, the answer often surprises people. The math usually reveals that a single server handles the load easily.

  • Add the capacity estimation section to the end of docs/requirements.md:

## Capacity Estimation

### Target System
- 5 floors, 500 spots per floor = 2,500 total spots.
- Operating hours: 16 hours/day (6 AM - 10 PM).

### Storage Estimates
- Spot record: ~100 bytes (id, floor_id, type, status, version, timestamps).
- 2,500 spots x 100 bytes = 250 KB (negligible).
- Ticket record: ~200 bytes.
- Peak day: 2,500 entries x 200 bytes = 500 KB/day.
- Annual tickets: ~900,000 records x 200 bytes = ~180 MB/year.

### Peak TPS (Transactions Per Second)
- Rush hour: 80% of entries occur within 4 peak hours (2 morning + 2 evening).
- 2,500 x 0.8 = 2,000 vehicles in 4 peak hours.
- Peak TPS: 2,000 / (4 x 3,600) = ~0.14 TPS average.
- Burst: assume 10x burst factor = ~1.4 TPS peak.
- Conclusion: a single server handles this easily. Scalability matters at multi-facility scale.

### Allocation Algorithm Tradeoff
- **Linear scan O(n)**: iterate all spots on a floor until a free compatible one is found.
  - Simple to implement. Acceptable for 500 spots per floor or fewer.
- **Min-heap O(log n)**: maintain a priority queue per spot type, sorted by proximity to entrance.
  - Better for 1,000+ spots per floor or when nearest-to-entrance routing is required.
- **Decision**: Use a Set-based grouped approach (O(1) existence check per type, O(n) within a type) for this implementation. Upgrade to min-heap when nearest-spot routing is needed.

Why does the algorithm tradeoff matter?

In a system design interview, interviewers want to see that you considered alternatives and made a justified choice. The peak TPS of ~1.4 proves this system is not compute-bound. The bottleneck is concurrency (two gates racing for one spot), not throughput.

Your implementation will use a Set grouped by spot type. This gives O(1) to check if any spot of a given type exists, then linear selection within that type. For 500 spots per floor, this is fast enough.

  • Save docs/requirements.md.
  • Check the Markdown preview. Your document should now have three top-level sections: Functional Requirements, Non-Functional Requirements, and Capacity Estimation.
  • Confirm the file exists in your project by running this command in the terminal:
ls docs/

You should see requirements.md listed in the output.

Don't see requirements.md?

Make sure you created the file inside the docs/ folder, not in the project root. Check that your terminal is in the parking-lot-system directory when running ls docs/.

help me find where my requirements.md file ended up

✔️ Awesome, I've got everything!

Great. Make sure you have saved docs/requirements.md with all three sections.

ⓧ I'd like to double check the full code

Here is the complete docs/requirements.md file:

# Parking Lot System - Requirements Specification

## Functional Requirements

### FR-1: Multi-Level Structure
- The system manages a parking lot with multiple floors.
- Each floor contains a configurable number of parking spots.

### FR-2: Vehicle Types
- The system supports four vehicle categories: Motorcycle, Car, Truck, and Electric.
- Each vehicle is identified by a unique license plate.

### FR-3: Spot Types and Compatibility
- Four spot types exist: Motorcycle, Compact, Large, and EV Charging.
- Compatibility rules:
  - Motorcycle: fits Motorcycle, Compact, or Large spots.
  - Car: fits Compact or Large spots.
  - Truck: fits only Large spots.
  - Electric: fits only EV Charging spots.

### FR-4: Entry Flow
- A vehicle arrives at an entry gate.
- The system assigns the smallest compatible spot (preference order: exact fit first).
- A ticket is issued containing the vehicle, assigned spot, and entry timestamp.

### FR-5: Exit Flow
- On exit, the system calculates the fee based on duration and vehicle type.
- The spot is released back to the available pool.
- The ticket is marked as paid with the calculated amount.

### FR-6: Real-Time Availability
- The system tracks available spots per type per floor in real time.
- Display boards at entrances show current availability.

### FR-7: Multiple Gates
- Multiple entry and exit gates operate concurrently.
- Two gates must never assign the same spot to different vehicles.

## Non-Functional Requirements

### NFR-1: Low Latency
- Gate operations (entry/exit) must complete in sub-100ms.
- Spot lookup and assignment must be optimized for speed.

### NFR-2: Thread Safety
- Concurrent access from multiple gates must be handled safely.
- Optimistic locking on spot assignment prevents double-allocation.

### NFR-3: Horizontal Scalability
- The system shards by lot_id for multi-facility deployments.
- Each lot operates independently with its own state.

### NFR-4: Fault Tolerance
- Edge gate controllers can operate offline with local state.
- Sync occurs when connectivity is restored.

### NFR-5: ACID Compliance
- All financial transactions (fee calculation, payment processing) are ACID-compliant.
- No partial ticket closures or lost payments.

## Capacity Estimation

### Target System
- 5 floors, 500 spots per floor = 2,500 total spots.
- Operating hours: 16 hours/day (6 AM - 10 PM).

### Storage Estimates
- Spot record: ~100 bytes (id, floor_id, type, status, version, timestamps).
- 2,500 spots x 100 bytes = 250 KB (negligible).
- Ticket record: ~200 bytes.
- Peak day: 2,500 entries x 200 bytes = 500 KB/day.
- Annual tickets: ~900,000 records x 200 bytes = ~180 MB/year.

### Peak TPS (Transactions Per Second)
- Rush hour: 80% of entries occur within 4 peak hours (2 morning + 2 evening).
- 2,500 x 0.8 = 2,000 vehicles in 4 peak hours.
- Peak TPS: 2,000 / (4 x 3,600) = ~0.14 TPS average.
- Burst: assume 10x burst factor = ~1.4 TPS peak.
- Conclusion: a single server handles this easily. Scalability matters at multi-facility scale.

### Allocation Algorithm Tradeoff
- **Linear scan O(n)**: iterate all spots on a floor until a free compatible one is found.
  - Simple to implement. Acceptable for 500 spots per floor or fewer.
- **Min-heap O(log n)**: maintain a priority queue per spot type, sorted by proximity to entrance.
  - Better for 1,000+ spots per floor or when nearest-to-entrance routing is required.
- **Decision**: Use a Set-based grouped approach (O(1) existence check per type, O(n) within a type) for this implementation. Upgrade to min-heap when nearest-spot routing is needed.

Your requirements specification is complete. You now have a clear contract for the system's behavior, constraints, and scale. Next up, you will translate these requirements into a high-level architecture with component diagrams, REST API contracts, and a relational database schema.

Create the High Level Design

You have a solid requirements document that defines what the parking lot system needs to do. Now you need to define how it does it at the architectural level.

A High Level Design (HLD) answers three questions: which services exist and how they communicate, what API contracts clients use to interact with the system, and how data is persisted. You will document all three in a single design artifact.

In this step, get ready to:
  • Design the system architecture with component breakdown and data flows.
  • Define REST API contracts for all parking operations.
  • Design the relational database schema with indexing strategy.
Design the system architecture

A parking lot system has physical components (gates, sensors, display boards) and backend services that coordinate them. The architecture separates these into distinct services, each with a single responsibility.

  • Create a new file called hld.md inside your docs/ folder.
  • Add the following architecture section to document the system components and data flows:
# High Level Design - Parking Lot System

## System Architecture

### Components

1. **Entry/Exit Gate Controllers (Edge)** - Low-latency edge nodes at each
   physical gate. Handle vehicle detection, ticket issuance/validation, and
   barrier control. Operate offline with a local queue when the network is
   unavailable.

2. **Slot Management Service** - Core service tracking real-time spot
   availability across all floors. Handles spot assignment with optimistic
   locking for concurrent access from multiple gates.

3. **Ticketing Service** - Issues digital tickets linking a vehicle to an
   assigned spot and entry timestamp. Validates tickets on exit and triggers
   fee calculation.

4. **Payment Service** - Strategy-based pricing engine. Calculates fees based
   on vehicle type and duration. Integrates with payment gateways. Enforces
   ACID transactions for all financial operations.

5. **Notification Service** - Observer-based event bus. Pushes availability
   updates to display boards, mobile apps, and analytics pipelines in
   real time.

6. **Admin Dashboard** - System health monitoring, revenue reporting,
   floor/spot configuration, and manual overrides.

### Data Flow - Vehicle Entry

1. Vehicle arrives at Entry Gate Controller
2. Gate Controller captures license plate (camera or manual input)
3. Gate Controller sends entry request to Slot Management Service
4. Slot Management Service finds available spot (smallest compatible first)
5. Slot Management Service reserves spot with optimistic lock
6. Ticketing Service issues ticket (vehicle + spot + timestamp)
7. Notification Service updates display boards with new availability
8. Gate barrier opens

### Data Flow - Vehicle Exit

1. Vehicle arrives at Exit Gate Controller
2. Driver presents ticket (scan or manual entry)
3. Gate Controller sends exit request with ticket ID
4. Ticketing Service retrieves ticket and calculates duration
5. Payment Service applies pricing strategy and calculates fee
6. Driver completes payment
7. Slot Management Service releases spot and increments version
8. Notification Service updates display boards
9. Gate barrier opens

What does this architecture cover?

The six components map to the services you will implement in code later. The Gate Controllers are the edge layer (closest to physical hardware). The Slot Management and Ticketing services are the core domain. Payment and Notification are supporting services.

The data flows document the exact sequence of events for the two critical operations in the system. Every step in these flows will become a method call in your Low Level Design.

Why edge controllers instead of a centralized gateway?

Physical parking gates cannot afford to be unresponsive. If the network drops, an edge controller with a local queue can still raise the barrier and sync tickets later. A centralized gateway would mean a network outage blocks every car at the gate.

  • Save docs/hld.md.
  • Confirm the file appears in your docs/ folder alongside requirements.md.
Design the REST API contracts

The REST API is the contract between clients (gate controllers, mobile apps, admin dashboard) and backend services. Defining it upfront means frontend and backend teams can work in parallel.

  • Add the following API contracts section below the architecture section in docs/hld.md:

## REST API Contracts

### POST /api/v1/parking/entry

Issue a ticket and assign a spot to an entering vehicle.

Request body:

    {
      "licensePlate": "ABC-1234",
      "vehicleType": "CAR"
    }

Response (201 Created):

    {
      "ticketId": "uuid-here",
      "spotId": "F1-S21",
      "floor": 1,
      "spotType": "COMPACT",
      "entryTime": "2024-01-15T10:30:00Z"
    }

### POST /api/v1/parking/exit

Calculate fee and release the spot for an exiting vehicle.

Request body:

    {
      "ticketId": "uuid-here"
    }

Response (200 OK):

    {
      "ticketId": "uuid-here",
      "licensePlate": "ABC-1234",
      "durationMinutes": 125,
      "fee": 60,
      "spotId": "F1-S21"
    }

### POST /api/v1/payments

Process payment for a parking ticket.

Request body:

    {
      "ticketId": "uuid-here",
      "paymentMethod": "CARD"
    }

Response (200 OK):

    {
      "receiptId": "receipt-uuid",
      "amount": 60,
      "status": "COMPLETED"
    }

### GET /api/v1/spots/availability

Get real-time spot counts by type and floor.

Response (200 OK):

    {
      "floors": [
        {
          "floorNumber": 1,
          "spots": {
            "MOTORCYCLE": 10,
            "COMPACT": 18,
            "LARGE": 5
          }
        }
      ],
      "totalAvailable": 95
    }

### POST /api/v1/admin/floors

Add or configure a new floor.

Request body:

    {
      "floorNumber": 4,
      "spots": [
        { "type": "COMPACT", "count": 30 },
        { "type": "LARGE", "count": 10 }
      ]
    }

Response (201 Created):

    {
      "floorId": "floor-uuid",
      "floorNumber": 4,
      "totalSpots": 40,
      "status": "OPEN"
    }

What do these endpoints cover?

Five endpoints cover the full lifecycle. POST /api/v1/parking/entry and POST /api/v1/parking/exit are the core gate operations called for every vehicle.

POST /api/v1/payments handles the financial transaction separately from the exit flow. GET /api/v1/spots/availability powers display boards and mobile apps. POST /api/v1/admin/floors lets operators configure the lot without code changes.

  • Save docs/hld.md.
Design the relational database schema

The database schema defines how the system persists state. Every entity in your architecture (lots, floors, spots, tickets) needs a table with clear relationships and constraints.

  • Add the following database schema section at the bottom of docs/hld.md:

## Database Schema

### parking_lots

| Column       | Type         | Constraints   |
|--------------|--------------|---------------|
| lot_id       | UUID         | PRIMARY KEY   |
| name         | VARCHAR(100) | NOT NULL      |
| address      | VARCHAR(255) | NOT NULL      |
| total_floors | INT          | NOT NULL      |

### parking_floors

| Column       | Type         | Constraints                       |
|--------------|--------------|-----------------------------------|
| floor_id     | UUID         | PRIMARY KEY                       |
| lot_id       | UUID         | FOREIGN KEY -> parking_lots       |
| floor_number | INT          | NOT NULL                          |
| status       | ENUM         | 'OPEN', 'FULL', 'MAINTENANCE'    |

### parking_spots

| Column      | Type | Constraints                                    |
|-------------|------|------------------------------------------------|
| spot_id     | UUID | PRIMARY KEY                                    |
| floor_id    | UUID | FOREIGN KEY -> parking_floors                  |
| spot_number | INT  | NOT NULL                                       |
| type        | ENUM | 'MOTORCYCLE', 'COMPACT', 'LARGE', 'EV_CHARGING' |
| status      | ENUM | 'FREE', 'OCCUPIED'                             |
| version     | INT  | DEFAULT 0 (used for optimistic locking)        |

### tickets

| Column       | Type          | Constraints                          |
|--------------|---------------|--------------------------------------|
| ticket_id    | UUID          | PRIMARY KEY                          |
| spot_id      | UUID          | FOREIGN KEY -> parking_spots         |
| vehicle_plate| VARCHAR(20)   | NOT NULL                             |
| vehicle_type | ENUM          | 'MOTORCYCLE', 'CAR', 'TRUCK', 'ELECTRIC' |
| entry_time   | TIMESTAMP     | NOT NULL                             |
| exit_time    | TIMESTAMP     | NULL (filled on exit)                |
| amount       | DECIMAL(10,2) | DEFAULT 0                            |
| status       | ENUM          | 'ACTIVE', 'PAID'                     |

### Indexing Strategy

- **idx_tickets_plate** - INDEX on tickets(vehicle_plate) for fast lookup
  by license plate during entry/exit validation
- **idx_tickets_spot** - INDEX on tickets(spot_id) to find the active ticket
  for a specific spot
- **idx_spots_floor_status** - COMPOSITE INDEX on
  parking_spots(floor_id, status) for efficient per-floor availability queries
- **idx_spots_type_status** - COMPOSITE INDEX on
  parking_spots(type, status) to find available spots of a specific type

### Sharding Strategy

Shard by lot_id for multi-facility deployments. Each lot's data is
self-contained (floors, spots, and tickets all belong to one lot), making
lot_id a natural shard key with no cross-shard queries for normal operations.

Why a version column on parking_spots?

The version column enables optimistic locking. When two gates try to assign the same spot simultaneously, each reads the current version. The first write increments it. The second write sees a version mismatch and retries with a different spot.

This avoids row-level locks that would slow down gate operations. You will implement this exact pattern in JavaScript in a later step.

Why index on both license_plate and spot_id?

The two most frequent queries are: "find the ticket for this license plate" (at entry/exit gates) and "is this spot currently occupied" (during spot assignment). Without indexes on these columns, both queries would require full table scans as the tickets table grows.

  • Save docs/hld.md.
  • Scroll through the full document. You should see three major sections: System Architecture, REST API Contracts, and Database Schema.

Formatting looks broken in preview?

Make sure your markdown tables have the pipe characters (|) aligned and that there is a blank line before each table. VS Code's markdown preview (Cmd+Shift+V on macOS, Ctrl+Shift+V on Windows) renders tables if the header separator row uses dashes.

Help me fix my markdown table formatting in VS Code.

Your High Level Design is complete. You have a system architecture that separates concerns into six services, API contracts that define how clients interact with the system, and a database schema with optimistic locking for concurrency. Next up, you will translate this architecture into actual JavaScript classes with a full domain model.

Build the Domain Model

Your HLD documents the architecture. Now it's time to translate that thinking into working code, starting with the domain model.

The domain model defines the core entities your system operates on: vehicles, parking spots, and tickets. Getting these classes right establishes the foundation for every design pattern and service you'll build in later steps.

In this step, get ready to:
  • Create type-safe enums and the Vehicle class hierarchy.
  • Build the ParkingSpot class with compatibility rules and optimistic locking.
  • Implement the Ticket value object for session tracking.
Implement the Vehicle hierarchy

Every entity in the parking system needs a defined type. JavaScript doesn't have built-in enums, but Object.freeze() creates objects whose properties can't be modified at runtime.

  • Create a new file called enums.js inside the src/models/ folder, then paste the following code:
export const VehicleType = Object.freeze({
  MOTORCYCLE: 'MOTORCYCLE',
  CAR: 'CAR',
  TRUCK: 'TRUCK',
  ELECTRIC: 'ELECTRIC'
});

export const SpotType = Object.freeze({
  MOTORCYCLE: 'MOTORCYCLE',
  COMPACT: 'COMPACT',
  LARGE: 'LARGE',
  EV_CHARGING: 'EV_CHARGING'
});

export const SpotStatus = Object.freeze({
  FREE: 'FREE',
  OCCUPIED: 'OCCUPIED'
});

export const TicketStatus = Object.freeze({
  ACTIVE: 'ACTIVE',
  PAID: 'PAID'
});

What does this code do?

Each Object.freeze() call creates an immutable object that acts as a type-safe enum. Once frozen, no one can add, remove, or change properties on these objects.

You're defining four enums: VehicleType for the kinds of vehicles, SpotType for parking space categories, SpotStatus for whether a spot is free or taken, and TicketStatus for the lifecycle of a parking session.

  • Save enums.js.
  • VS Code should show no red underlines in the file.

Seeing red underlines?

Check for missing commas between properties and missing closing braces. Each frozen object needs a matching }); at the end.

help me fix syntax errors in my enums.js file

✔️ Awesome, I've got everything!

Great. Double check you've saved enums.js before moving on.

ⓧ I'd like to double check the full code

export const VehicleType = Object.freeze({
  MOTORCYCLE: 'MOTORCYCLE',
  CAR: 'CAR',
  TRUCK: 'TRUCK',
  ELECTRIC: 'ELECTRIC'
});

export const SpotType = Object.freeze({
  MOTORCYCLE: 'MOTORCYCLE',
  COMPACT: 'COMPACT',
  LARGE: 'LARGE',
  EV_CHARGING: 'EV_CHARGING'
});

export const SpotStatus = Object.freeze({
  FREE: 'FREE',
  OCCUPIED: 'OCCUPIED'
});

export const TicketStatus = Object.freeze({
  ACTIVE: 'ACTIVE',
  PAID: 'PAID'
});

Now that the type system is in place, you can build the Vehicle class hierarchy that uses these enums.

  • Create a new file called Vehicle.js inside src/models/, then paste the following code:
import { VehicleType } from './enums.js';

export class Vehicle {
  constructor(licensePlate, type) {
    if (new.target === Vehicle) {
      throw new Error('Vehicle is abstract and cannot be instantiated directly');
    }
    this.licensePlate = licensePlate;
    this.type = type;
  }
}

export class Motorcycle extends Vehicle {
  constructor(licensePlate) {
    super(licensePlate, VehicleType.MOTORCYCLE);
  }
}

export class Car extends Vehicle {
  constructor(licensePlate) {
    super(licensePlate, VehicleType.CAR);
  }
}

export class Truck extends Vehicle {
  constructor(licensePlate) {
    super(licensePlate, VehicleType.TRUCK);
  }
}

export class ElectricVehicle extends Vehicle {
  constructor(licensePlate) {
    super(licensePlate, VehicleType.ELECTRIC);
  }
}

What does this code do?

The base Vehicle class uses new.target to simulate an abstract class. If someone tries new Vehicle(...) directly, it throws an error. Only subclasses like Car or Truck can be instantiated.

Each subclass calls super() with its specific VehicleType enum value. This means consumers never need to manually pass a type string.

  • Save Vehicle.js.
  • Confirm both model files exist by running:
ls src/models/

You should see enums.js and Vehicle.js listed in the output.

Import error on the first line?

Make sure your package.json has "type": "module" set. Without it, Node.js treats .js files as CommonJS and won't recognize import statements.

help me fix ES module import errors in my Vehicle.js file

✔️ Awesome, I've got everything!

Great. Double check you've saved Vehicle.js before moving on.

ⓧ I'd like to double check the full code

import { VehicleType } from './enums.js';

export class Vehicle {
  constructor(licensePlate, type) {
    if (new.target === Vehicle) {
      throw new Error('Vehicle is abstract and cannot be instantiated directly');
    }
    this.licensePlate = licensePlate;
    this.type = type;
  }
}

export class Motorcycle extends Vehicle {
  constructor(licensePlate) {
    super(licensePlate, VehicleType.MOTORCYCLE);
  }
}

export class Car extends Vehicle {
  constructor(licensePlate) {
    super(licensePlate, VehicleType.CAR);
  }
}

export class Truck extends Vehicle {
  constructor(licensePlate) {
    super(licensePlate, VehicleType.TRUCK);
  }
}

export class ElectricVehicle extends Vehicle {
  constructor(licensePlate) {
    super(licensePlate, VehicleType.ELECTRIC);
  }
}
Build the ParkingSpot class with compatibility rules

Parking spots aren't interchangeable. A motorcycle can fit anywhere, but a truck can only park in a large spot. The ParkingSpot class encodes these rules and adds optimistic locking for safe concurrent access.

  • Create a new file called ParkingSpot.js inside src/models/, then paste the following code:
import { SpotType, SpotStatus, VehicleType } from './enums.js';

const COMPATIBILITY = {
  [SpotType.MOTORCYCLE]: [VehicleType.MOTORCYCLE],
  [SpotType.COMPACT]: [VehicleType.MOTORCYCLE, VehicleType.CAR],
  [SpotType.LARGE]: [VehicleType.MOTORCYCLE, VehicleType.CAR, VehicleType.TRUCK],
  [SpotType.EV_CHARGING]: [VehicleType.ELECTRIC]
};

export class ParkingSpot {
  constructor(id, floor, type) {
    this.id = id;
    this.floor = floor;
    this.type = type;
    this.status = SpotStatus.FREE;
    this.vehicle = null;
    this.version = 0;
  }

  canAccommodate(vehicleType) {
    return COMPATIBILITY[this.type]?.includes(vehicleType) ?? false;
  }

  parkVehicle(vehicle, expectedVersion) {
    if (this.status !== SpotStatus.FREE) {
      return false;
    }
    if (this.version !== expectedVersion) {
      return false;
    }
    if (!this.canAccommodate(vehicle.type)) {
      return false;
    }
    this.vehicle = vehicle;
    this.status = SpotStatus.OCCUPIED;
    this.version += 1;
    return true;
  }

  vacate() {
    const vehicle = this.vehicle;
    this.vehicle = null;
    this.status = SpotStatus.FREE;
    this.version += 1;
    return vehicle;
  }

  get isAvailable() {
    return this.status === SpotStatus.FREE;
  }
}

What does this code do?

The COMPATIBILITY map defines which vehicle types fit in which spot types. A COMPACT spot accepts motorcycles and cars. A LARGE spot accepts everything except electric vehicles (those need dedicated chargers).

The parkVehicle() method uses optimistic locking via the version field. When two gates try to assign the same spot at the same time, the second one will see a version mismatch and fail gracefully. This avoids database-level row locks in a distributed system.

  • Save ParkingSpot.js.
  • VS Code should show no red underlines. The import from ./enums.js should resolve without errors since both files are in the same folder.

Red underline on the COMPATIBILITY map?

Make sure you're using square brackets around the keys ([SpotType.MOTORCYCLE]) for computed property names. Without the brackets, JavaScript treats them as literal string keys instead of evaluating the enum value.

help me fix errors in my ParkingSpot.js COMPATIBILITY map

✔️ Awesome, I've got everything!

Great. Double check you've saved ParkingSpot.js before moving on.

ⓧ I'd like to double check the full code

import { SpotType, SpotStatus, VehicleType } from './enums.js';

const COMPATIBILITY = {
  [SpotType.MOTORCYCLE]: [VehicleType.MOTORCYCLE],
  [SpotType.COMPACT]: [VehicleType.MOTORCYCLE, VehicleType.CAR],
  [SpotType.LARGE]: [VehicleType.MOTORCYCLE, VehicleType.CAR, VehicleType.TRUCK],
  [SpotType.EV_CHARGING]: [VehicleType.ELECTRIC]
};

export class ParkingSpot {
  constructor(id, floor, type) {
    this.id = id;
    this.floor = floor;
    this.type = type;
    this.status = SpotStatus.FREE;
    this.vehicle = null;
    this.version = 0;
  }

  canAccommodate(vehicleType) {
    return COMPATIBILITY[this.type]?.includes(vehicleType) ?? false;
  }

  parkVehicle(vehicle, expectedVersion) {
    if (this.status !== SpotStatus.FREE) {
      return false;
    }
    if (this.version !== expectedVersion) {
      return false;
    }
    if (!this.canAccommodate(vehicle.type)) {
      return false;
    }
    this.vehicle = vehicle;
    this.status = SpotStatus.OCCUPIED;
    this.version += 1;
    return true;
  }

  vacate() {
    const vehicle = this.vehicle;
    this.vehicle = null;
    this.status = SpotStatus.FREE;
    this.version += 1;
    return vehicle;
  }

  get isAvailable() {
    return this.status === SpotStatus.FREE;
  }
}
Implement the Ticket class

A ticket represents a single parking session. It links a vehicle to a spot and tracks the time window for fee calculation.

  • Create a new file called Ticket.js inside src/models/, then paste the following code:
import { randomUUID } from 'crypto';
import { TicketStatus } from './enums.js';

export class Ticket {
  constructor(vehicle, spot) {
    this.ticketId = randomUUID();
    this.vehicle = vehicle;
    this.spot = spot;
    this.entryTime = new Date();
    this.exitTime = null;
    this.amount = 0;
    this.status = TicketStatus.ACTIVE;
  }

  calculateDurationMinutes() {
    const end = this.exitTime || new Date();
    return Math.ceil((end - this.entryTime) / (1000 * 60));
  }

  close(amount) {
    this.exitTime = new Date();
    this.amount = amount;
    this.status = TicketStatus.PAID;
  }
}

What does this code do?

randomUUID() from Node's built-in crypto module generates a unique ticket ID without needing an external library.

calculateDurationMinutes() computes how long the vehicle has been parked by diffing timestamps. If the vehicle hasn't exited yet, it uses the current time as the endpoint. The close() method stamps the exit time and records the fee, transitioning the ticket to PAID status.

  • Save Ticket.js.
  • Verify all four model files load correctly by running:
node -e "Promise.all([import('./src/models/enums.js'), import('./src/models/Vehicle.js'), import('./src/models/ParkingSpot.js'), import('./src/models/Ticket.js')]).then(() => console.log('All domain models loaded successfully'))"

You should see All domain models loaded successfully printed in the terminal.

Seeing a module not found error?

Make sure you're running the command from the parking-lot-system project root folder. The relative paths (./src/models/...) only resolve from the correct directory.

Also confirm each file is saved with the exact filename shown: enums.js, Vehicle.js, ParkingSpot.js, Ticket.js. Capitalization matters.

help me fix module not found errors when loading my domain models

✔️ Awesome, I've got everything!

Great. Double check you've saved Ticket.js before moving on.

ⓧ I'd like to double check the full code

import { randomUUID } from 'crypto';
import { TicketStatus } from './enums.js';

export class Ticket {
  constructor(vehicle, spot) {
    this.ticketId = randomUUID();
    this.vehicle = vehicle;
    this.spot = spot;
    this.entryTime = new Date();
    this.exitTime = null;
    this.amount = 0;
    this.status = TicketStatus.ACTIVE;
  }

  calculateDurationMinutes() {
    const end = this.exitTime || new Date();
    return Math.ceil((end - this.entryTime) / (1000 * 60));
  }

  close(amount) {
    this.exitTime = new Date();
    this.amount = amount;
    this.status = TicketStatus.PAID;
  }
}

Your domain model is complete. Next up, you'll bring these classes to life with five design patterns: Strategy for pricing, Factory for object creation, Singleton for the lot instance, Observer for notifications, and Facade for orchestration.

Implement the Design Patterns

Your domain model defines what objects exist in the parking lot system. Design patterns define how those objects get created, how they interact, and how their behavior can change at runtime without modifying existing code.

In this step, you'll implement four design patterns that form the architectural backbone of the system. Each pattern solves a specific structural problem: Strategy handles variable pricing, Factory manages object creation, and Observer decouples notifications from core logic.

In this step, get ready to:
  • Implement the Strategy pattern with four interchangeable pricing algorithms.
  • Implement the Factory pattern for decoupled vehicle and spot creation.
  • Implement the Observer pattern for real-time availability notifications.
Implement the Strategy pattern for pricing

Parking lots need different pricing models: hourly rates for short stays, flat daily rates for long stays, surge pricing on weekends. Without the Strategy pattern, you'd need if/else chains that grow every time a new pricing rule appears.

The Strategy pattern encapsulates each pricing algorithm behind a common interface. The parking lot calls calculateFee() without knowing which strategy is active. Swapping pricing at runtime requires zero changes to core logic.

  • Create a new file src/patterns/PricingStrategy.js and add the base class with the first two standalone strategies:
import { VehicleType } from '../models/enums.js';

export class PricingStrategy {
  calculateFee(vehicleType, durationMinutes) {
    throw new Error('calculateFee must be implemented by subclass');
  }
}

export class HourlyPricingStrategy extends PricingStrategy {
  constructor() {
    super();
    // Per-hour rates by vehicle type
    this.rates = {
      [VehicleType.MOTORCYCLE]: 10,
      [VehicleType.CAR]: 20,
      [VehicleType.TRUCK]: 30,
      [VehicleType.ELECTRIC]: 25
    };
  }

  calculateFee(vehicleType, durationMinutes) {
    // Round up to the nearest hour
    const hours = Math.ceil(durationMinutes / 60);
    const rate = this.rates[vehicleType] || 20;
    return hours * rate;
  }
}

export class FlatRatePricingStrategy extends PricingStrategy {
  constructor() {
    super();
    // Fixed daily rates regardless of duration
    this.rates = {
      [VehicleType.MOTORCYCLE]: 50,
      [VehicleType.CAR]: 100,
      [VehicleType.TRUCK]: 150,
      [VehicleType.ELECTRIC]: 80
    };
  }

  calculateFee(vehicleType, durationMinutes) {
    return this.rates[vehicleType] || 100;
  }
}

What does this code do?

The base PricingStrategy class defines the interface contract. Any subclass must implement calculateFee() or the error gets thrown at runtime.

  • HourlyPricingStrategy rounds up to the nearest hour and multiplies by a vehicle-specific rate. A car parked for 90 minutes pays for 2 hours at $20/hour = $40.
  • FlatRatePricingStrategy ignores duration entirely and charges a fixed daily rate per vehicle type.

The real power of Strategy appears when you combine strategies. Add the decorator-style strategies below the FlatRatePricingStrategy class. These wrap another strategy and modify its output.

  • Add the following two classes at the bottom of src/patterns/PricingStrategy.js:
export class WeekendSurgePricingStrategy extends PricingStrategy {
  constructor(baseStrategy) {
    super();
    // Wraps any existing strategy and applies a weekend multiplier
    this.baseStrategy = baseStrategy;
    this.surgeMultiplier = 1.5;
  }

  calculateFee(vehicleType, durationMinutes) {
    const baseFee = this.baseStrategy.calculateFee(vehicleType, durationMinutes);
    const today = new Date().getDay();
    const isWeekend = today === 0 || today === 6;
    return isWeekend ? baseFee * this.surgeMultiplier : baseFee;
  }
}

export class EVDiscountPricingStrategy extends PricingStrategy {
  constructor(baseStrategy) {
    super();
    // Wraps any existing strategy and discounts electric vehicles
    this.baseStrategy = baseStrategy;
    this.discountRate = 0.8;
  }

  calculateFee(vehicleType, durationMinutes) {
    const baseFee = this.baseStrategy.calculateFee(vehicleType, durationMinutes);
    if (vehicleType === VehicleType.ELECTRIC) {
      return baseFee * this.discountRate;
    }
    return baseFee;
  }
}

What does this code do?

These two strategies wrap another strategy (passed as baseStrategy in the constructor) and modify its output. This is the Decorator pattern layered on top of Strategy.

  • WeekendSurgePricingStrategy applies a 1.5x multiplier on Saturdays and Sundays. On weekdays, it passes through the base fee unchanged.
  • EVDiscountPricingStrategy gives electric vehicles a 20% discount (0.8 multiplier) while charging all other vehicle types the standard rate.
  • Save src/patterns/PricingStrategy.js.
  • Verify the module loads without errors by running:
node src/patterns/PricingStrategy.js

You should see no output and no errors. The module loaded and exited cleanly.

Seeing a SyntaxError or import error?

  • Check that your import path is '../models/enums.js' with the relative path and .js extension. ES modules require explicit file extensions.
  • Make sure your package.json has "type": "module" set. Without this, Node.js treats files as CommonJS and import won't work.

Help me debug my PricingStrategy import error

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

import { VehicleType } from '../models/enums.js';

export class PricingStrategy {
  calculateFee(vehicleType, durationMinutes) {
    throw new Error('calculateFee must be implemented by subclass');
  }
}

export class HourlyPricingStrategy extends PricingStrategy {
  constructor() {
    super();
    this.rates = {
      [VehicleType.MOTORCYCLE]: 10,
      [VehicleType.CAR]: 20,
      [VehicleType.TRUCK]: 30,
      [VehicleType.ELECTRIC]: 25
    };
  }

  calculateFee(vehicleType, durationMinutes) {
    const hours = Math.ceil(durationMinutes / 60);
    const rate = this.rates[vehicleType] || 20;
    return hours * rate;
  }
}

export class FlatRatePricingStrategy extends PricingStrategy {
  constructor() {
    super();
    this.rates = {
      [VehicleType.MOTORCYCLE]: 50,
      [VehicleType.CAR]: 100,
      [VehicleType.TRUCK]: 150,
      [VehicleType.ELECTRIC]: 80
    };
  }

  calculateFee(vehicleType, durationMinutes) {
    return this.rates[vehicleType] || 100;
  }
}

export class WeekendSurgePricingStrategy extends PricingStrategy {
  constructor(baseStrategy) {
    super();
    this.baseStrategy = baseStrategy;
    this.surgeMultiplier = 1.5;
  }

  calculateFee(vehicleType, durationMinutes) {
    const baseFee = this.baseStrategy.calculateFee(vehicleType, durationMinutes);
    const today = new Date().getDay();
    const isWeekend = today === 0 || today === 6;
    return isWeekend ? baseFee * this.surgeMultiplier : baseFee;
  }
}

export class EVDiscountPricingStrategy extends PricingStrategy {
  constructor(baseStrategy) {
    super();
    this.baseStrategy = baseStrategy;
    this.discountRate = 0.8;
  }

  calculateFee(vehicleType, durationMinutes) {
    const baseFee = this.baseStrategy.calculateFee(vehicleType, durationMinutes);
    if (vehicleType === VehicleType.ELECTRIC) {
      return baseFee * this.discountRate;
    }
    return baseFee;
  }
}

Why Strategy instead of if/else?

You could implement all pricing in a single function with conditionals. That works until you need to add holiday pricing, membership discounts, or dynamic surge rates. Each addition forces changes to the same function, violating the Open/Closed Principle.

With Strategy, adding a new pricing model means creating a new class. Existing strategies remain untouched and proven.

Implement the Factory pattern for object creation

The Factory pattern centralizes object creation so that calling code never uses new Motorcycle() or new Car() directly. When you add a new vehicle type later, only the factory needs updating.

  • Create src/patterns/VehicleFactory.js with the following code:
import { VehicleType } from '../models/enums.js';
import { Motorcycle, Car, Truck, ElectricVehicle } from '../models/Vehicle.js';

export class VehicleFactory {
  // Static method returns the correct Vehicle subclass based on type
  static create(type, licensePlate) {
    switch (type) {
      case VehicleType.MOTORCYCLE:
        return new Motorcycle(licensePlate);
      case VehicleType.CAR:
        return new Car(licensePlate);
      case VehicleType.TRUCK:
        return new Truck(licensePlate);
      case VehicleType.ELECTRIC:
        return new ElectricVehicle(licensePlate);
      default:
        throw new Error(`Unknown vehicle type: ${type}`);
    }
  }
}

What does this code do?

VehicleFactory.create() is a static method that takes a VehicleType enum value and a license plate, then returns the appropriate Vehicle subclass instance. The calling code never needs to know which concrete class was instantiated.

  • Save src/patterns/VehicleFactory.js.
  • Verify the module loads cleanly by running:
node src/patterns/VehicleFactory.js

No output means the module loaded successfully with no syntax or import errors.

Getting a module not found error?

  • Verify that src/models/Vehicle.js exists and exports Motorcycle, Car, Truck, and ElectricVehicle.
  • Check that the import path is '../models/Vehicle.js' with the correct relative path from the patterns/ folder.

Help me fix my VehicleFactory import path

Now create the companion factory for parking spots. The SpotFactory validates the spot type before creating a ParkingSpot instance.

  • Create src/patterns/SpotFactory.js with the following code:
import { SpotType } from '../models/enums.js';
import { ParkingSpot } from '../models/ParkingSpot.js';

export class SpotFactory {
  // Validates the spot type before creating an instance
  static create(type, id, floor) {
    if (!Object.values(SpotType).includes(type)) {
      throw new Error(`Unknown spot type: ${type}`);
    }
    return new ParkingSpot(id, floor, type);
  }
}

What does this code do?

SpotFactory.create() uses Object.values(SpotType).includes(type) as a guard to reject invalid types before creating a spot. This prevents silent bugs where a typo in a type string creates an uncategorized spot.

  • Save src/patterns/SpotFactory.js.
  • Verify the module loads without errors by running:
node src/patterns/SpotFactory.js

No output confirms a clean load.

✔️ Awesome, I've got everything!

Great. Both factory files should be saved in your patterns folder.

ⓧ I'd like to double check the full code

Here is src/patterns/VehicleFactory.js:

import { VehicleType } from '../models/enums.js';
import { Motorcycle, Car, Truck, ElectricVehicle } from '../models/Vehicle.js';

export class VehicleFactory {
  static create(type, licensePlate) {
    switch (type) {
      case VehicleType.MOTORCYCLE:
        return new Motorcycle(licensePlate);
      case VehicleType.CAR:
        return new Car(licensePlate);
      case VehicleType.TRUCK:
        return new Truck(licensePlate);
      case VehicleType.ELECTRIC:
        return new ElectricVehicle(licensePlate);
      default:
        throw new Error(`Unknown vehicle type: ${type}`);
    }
  }
}

Here is src/patterns/SpotFactory.js:

import { SpotType } from '../models/enums.js';
import { ParkingSpot } from '../models/ParkingSpot.js';

export class SpotFactory {
  static create(type, id, floor) {
    if (!Object.values(SpotType).includes(type)) {
      throw new Error(`Unknown spot type: ${type}`);
    }
    return new ParkingSpot(id, floor, type);
  }
}
Implement the Singleton and Observer patterns

Two more patterns complete this step. The Singleton pattern ensures only one ParkingLot instance exists across the entire application. You'll implement this when you build the ParkingLot class in the next step using a private static field and a getInstance() method.

The Observer pattern decouples the notification mechanism from core parking logic. When a spot changes status, observers (like a physical display board) get notified automatically. Adding new notification targets later requires no changes to the parking system itself.

  • Create src/patterns/Observer.js with the observer base class and the DisplayBoard implementation:
export class ParkingObserver {
  onSpotUpdate(floor, spotType, availableCount) {
    throw new Error('onSpotUpdate must be implemented by subclass');
  }
}

export class DisplayBoard extends ParkingObserver {
  constructor(name) {
    super();
    // Identifies which display board this is (e.g., "Main Entrance")
    this.name = name;
  }

  onSpotUpdate(floor, spotType, availableCount) {
    console.log(
      `[${this.name}] Floor ${floor} | ${spotType}: ${availableCount} spots available`
    );
  }
}

What does this code do?

  • ParkingObserver is the abstract base. Any observer must implement onSpotUpdate(floor, spotType, availableCount) to react to spot status changes.
  • DisplayBoard is a concrete observer that logs availability to the console. In a real system, this would update a physical LED board at a parking entrance.

Adding a MobileAppNotifier or SMSAlertObserver later requires only a new class. The ParkingLot never needs to change.

  • Save src/patterns/Observer.js.
  • Verify the module loads without errors by running:
node src/patterns/Observer.js

No output confirms the module loaded cleanly.

Seeing a syntax error in Observer.js?

  • Check that the template literal in the console.log uses backticks (`) not regular quotes. Template literals require the backtick character for string interpolation.
  • Verify that DisplayBoard calls super() in its constructor. Without this, the class won't initialize properly.

Help me fix my Observer.js syntax error

✔️ Awesome, I've got everything!

Great. Double check you've saved the file before moving on.

ⓧ I'd like to double check the full code

export class ParkingObserver {
  onSpotUpdate(floor, spotType, availableCount) {
    throw new Error('onSpotUpdate must be implemented by subclass');
  }
}

export class DisplayBoard extends ParkingObserver {
  constructor(name) {
    super();
    this.name = name;
  }

  onSpotUpdate(floor, spotType, availableCount) {
    console.log(
      `[${this.name}] Floor ${floor} | ${spotType}: ${availableCount} spots available`
    );
  }
}
  • Confirm all four pattern files exist by running:
ls src/patterns/

You should see Observer.js, PricingStrategy.js, SpotFactory.js, and VehicleFactory.js listed.

Your design patterns are ready to wire together. In the next step, you'll build the service layer that uses these patterns: ParkingFloor for spot management, and ParkingLot as the Facade and Singleton that orchestrates the entire system.

Build the Services Layer

Your models define what things are. Your patterns define how to create and price them. But nothing actually happens yet.

The services layer orchestrates models and patterns into real workflows. In this step, you'll build ParkingFloor (manages spots on a single level) and ParkingLot (the Facade that coordinates the full parking lifecycle with concurrency protection).

In this step, get ready to:
  • Build the ParkingFloor class to manage spots per floor with O(1) availability checking.
  • Build the ParkingLot Facade with Singleton and Observer integration.
  • Add concurrency protection with optimistic locking.
Create the ParkingFloor class

A parking floor manages a collection of spots and needs to answer one critical question quickly: is there a compatible spot available for this vehicle type?

Instead of scanning every spot on every lookup, you'll group available spots by type in a Map of Sets. Checking whether a compatible type has spots is O(1). Selecting a spot within that type is O(1) as well (grab the first element from the Set).

  • Create a new file at src/services/ParkingFloor.js and paste the following code:
import { SpotType, SpotStatus, VehicleType } from '../models/enums.js';
import { SpotFactory } from '../patterns/SpotFactory.js';

const VEHICLE_SPOT_PREFERENCE = {
  [VehicleType.MOTORCYCLE]: [SpotType.MOTORCYCLE, SpotType.COMPACT, SpotType.LARGE],
  [VehicleType.CAR]: [SpotType.COMPACT, SpotType.LARGE],
  [VehicleType.TRUCK]: [SpotType.LARGE],
  [VehicleType.ELECTRIC]: [SpotType.EV_CHARGING]
};

export class ParkingFloor {
  constructor(floorNumber, spotConfigs) {
    this.floorNumber = floorNumber;
    this.spots = [];
    this.availableByType = new Map();

    Object.values(SpotType).forEach(type => {
      this.availableByType.set(type, new Set());
    });

    this.initializeSpots(spotConfigs);
  }

  initializeSpots(spotConfigs) {
    let spotId = 0;
    for (const { type, count } of spotConfigs) {
      for (let i = 0; i < count; i++) {
        spotId++;
        const id = `F${this.floorNumber}-S${spotId}`;
        const spot = SpotFactory.create(type, id, this.floorNumber);
        this.spots.push(spot);
        this.availableByType.get(type).add(spot);
      }
    }
  }

  findAvailableSpot(vehicleType) {
    const preferredTypes = VEHICLE_SPOT_PREFERENCE[vehicleType];
    if (!preferredTypes) return null;

    for (const spotType of preferredTypes) {
      const available = this.availableByType.get(spotType);
      if (available && available.size > 0) {
        const spot = available.values().next().value;
        return spot;
      }
    }
    return null;
  }

  assignSpot(spot, vehicle) {
    const success = spot.parkVehicle(vehicle, spot.version);
    if (success) {
      this.availableByType.get(spot.type).delete(spot);
    }
    return success;
  }

  releaseSpot(spot) {
    spot.vacate();
    this.availableByType.get(spot.type).add(spot);
  }

  getAvailableCount(spotType) {
    return this.availableByType.get(spotType)?.size || 0;
  }

  get totalAvailable() {
    let count = 0;
    for (const spots of this.availableByType.values()) {
      count += spots.size;
    }
    return count;
  }
}

What does this code do?

  • VEHICLE_SPOT_PREFERENCE defines which spot types each vehicle type can use, ordered smallest-compatible-first. A motorcycle tries a motorcycle spot before falling back to compact or large.
  • availableByType is a Map where each SpotType key points to a Set of free spots. Checking set.size > 0 is O(1), so you know instantly whether compatible spots exist.
  • assignSpot() delegates to the spot's parkVehicle(vehicle, spot.version) method, passing the current version for optimistic locking. If assignment succeeds, the spot is removed from the available set.
  • releaseSpot() calls vacate() on the spot and returns it to the available pool, ready for the next vehicle.
  • Save src/services/ParkingFloor.js.

You won't see output yet. The next chunk builds the ParkingLot that uses this floor.

Why a Map of Sets instead of a flat array?

A flat array of all spots would require scanning every spot to find an available one (O(n) where n = total spots on the floor). With 500 spots per floor, that adds up.

The Map-of-Sets approach gives you O(1) existence checks (is there ANY compact spot free?) and O(1) selection (grab the first one from the Set). You only pay O(n) when iterating within a type, and that set is much smaller than the total.

Create the ParkingLot Facade

The ParkingLot class is the single coordination point for the entire system. It combines three patterns: Singleton (one lot instance), Facade (hides complex interactions behind simple methods), and Observer (notifies display boards on changes).

  • Create a new file at src/services/ParkingLot.js and paste the following code:
import { Ticket } from '../models/Ticket.js';

export class ParkingLot {
  static #instance = null;

  constructor(pricingStrategy) {
    if (ParkingLot.#instance) {
      throw new Error('ParkingLot is a singleton. Use ParkingLot.getInstance()');
    }
    this.floors = [];
    this.activeTickets = new Map();
    this.pricingStrategy = pricingStrategy;
    this.observers = [];
  }

  static getInstance(pricingStrategy) {
    if (!ParkingLot.#instance) {
      ParkingLot.#instance = new ParkingLot(pricingStrategy);
    }
    return ParkingLot.#instance;
  }

  static resetInstance() {
    ParkingLot.#instance = null;
  }

  addFloor(floor) {
    this.floors.push(floor);
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  removeObserver(observer) {
    this.observers = this.observers.filter(o => o !== observer);
  }

  notifyObservers(floor, spotType, availableCount) {
    for (const observer of this.observers) {
      observer.onSpotUpdate(floor, spotType, availableCount);
    }
  }

  setPricingStrategy(strategy) {
    this.pricingStrategy = strategy;
    console.log(`[ParkingLot] Pricing strategy changed to: ${strategy.constructor.name}`);
  }

  enterVehicle(vehicle) {
    for (const floor of this.floors) {
      const spot = floor.findAvailableSpot(vehicle.type);
      if (!spot) continue;

      const currentVersion = spot.version;
      const success = floor.assignSpot(spot, vehicle);

      if (!success) {
        console.log(`[ParkingLot] Optimistic lock conflict on spot ${spot.id}. Retrying...`);
        return this.enterVehicle(vehicle);
      }

      const ticket = new Ticket(vehicle, spot);
      this.activeTickets.set(ticket.ticketId, ticket);

      this.notifyObservers(
        floor.floorNumber,
        spot.type,
        floor.getAvailableCount(spot.type)
      );

      console.log(
        `[ParkingLot] Vehicle ${vehicle.licensePlate} (${vehicle.type}) parked at spot ${spot.id}`
      );
      return ticket;
    }

    throw new Error(`No available spot for vehicle type: ${vehicle.type}`);
  }

  exitVehicle(ticketId) {
    const ticket = this.activeTickets.get(ticketId);
    if (!ticket) {
      throw new Error(`Invalid ticket: ${ticketId}`);
    }

    const durationMinutes = ticket.calculateDurationMinutes();
    const fee = this.pricingStrategy.calculateFee(ticket.vehicle.type, durationMinutes);

    const floor = this.floors.find(f => f.floorNumber === ticket.spot.floor);
    floor.releaseSpot(ticket.spot);

    ticket.close(fee);
    this.activeTickets.delete(ticketId);

    this.notifyObservers(
      floor.floorNumber,
      ticket.spot.type,
      floor.getAvailableCount(ticket.spot.type)
    );

    console.log(
      `[ParkingLot] Vehicle ${ticket.vehicle.licensePlate} exited. Duration: ${durationMinutes}min, Fee: $${fee}`
    );
    return ticket;
  }

  getAvailability() {
    const availability = {};
    for (const floor of this.floors) {
      availability[`Floor ${floor.floorNumber}`] = {};
      for (const [type, spots] of floor.availableByType) {
        if (spots.size > 0 || floor.spots.some(s => s.type === type)) {
          availability[`Floor ${floor.floorNumber}`][type] = spots.size;
        }
      }
    }
    return availability;
  }
}

What does this code do?

  • static #instance enforces the Singleton pattern. The constructor throws if an instance already exists, and getInstance() creates one only on the first call. resetInstance() exists for testability.
  • enterVehicle() is the Facade for the entry flow: find a spot across floors, assign it with optimistic locking, create a ticket, and notify observers. If the lock fails (another gate grabbed the spot), it retries recursively.
  • exitVehicle() orchestrates the exit: calculate the fee via the injected pricing strategy, release the spot, close the ticket, and notify observers of the freed capacity.
  • setPricingStrategy() enables runtime strategy swaps. You can switch from hourly to surge pricing without restarting the system.
  • getAvailability() builds a snapshot of available spots per floor and type, suitable for display boards or API responses.
  • Save src/services/ParkingLot.js.

You won't see output yet. In the next step, you'll wire everything together in main.js and run the full parking lifecycle.

Seeing an import error?

  • Make sure ParkingLot.js is inside src/services/ (not src/models/ or src/patterns/).
  • Check that the relative import path to Ticket.js uses '../models/Ticket.js' (going up one level from services to src, then into models).
  • Verify your package.json still has "type": "module" so that ES module imports work.

Help me debug my ParkingLot import error

Understand the concurrency protection

In a real parking lot, multiple entry gates operate simultaneously. Two cars arriving at the same moment could both target the same spot. Without protection, both would "succeed" and you'd have a data corruption bug.

Your code implements optimistic locking using the version field on each ParkingSpot. Here's how the three layers work together:

How does optimistic locking flow through the system?

The mechanism spans three classes, each with a single responsibility:

  • ParkingSpot.parkVehicle(vehicle, expectedVersion) checks this.version !== expectedVersion. If another gate already incremented the version, the check fails and returns false. This is the lock guard.
  • ParkingFloor.assignSpot(spot, vehicle) passes spot.version into parkVehicle. If parkVehicle fails, it does NOT remove the spot from the available set. The floor stays consistent.
  • ParkingLot.enterVehicle(vehicle) detects the failure and recursively calls itself. On the retry, findAvailableSpot returns the next available spot (the conflicted one is now occupied).

In a distributed system, this version field maps directly to a database column with a WHERE version = expected_version clause in your UPDATE query. Zero rows affected means a conflict occurred.

Why optimistic locking instead of a mutex?

JavaScript is single-threaded, so you might wonder why this matters. In production, multiple gate servers (separate Node.js processes or even separate machines) write to the same database. A mutex only works within a single process.

Optimistic locking works across processes because the version check happens at the data layer (the spot itself). No shared-memory lock is needed. The tradeoff is occasional retries under contention, but for a parking lot with hundreds of spots and a handful of gates, conflicts are rare.

✔️ Awesome, I've got everything!

Great. Double check you've saved both files in the src/services/ folder.

ⓧ I'd like to double check the full code

Here's the complete code for both service files.

import { SpotType, SpotStatus, VehicleType } from '../models/enums.js';
import { SpotFactory } from '../patterns/SpotFactory.js';

const VEHICLE_SPOT_PREFERENCE = {
  [VehicleType.MOTORCYCLE]: [SpotType.MOTORCYCLE, SpotType.COMPACT, SpotType.LARGE],
  [VehicleType.CAR]: [SpotType.COMPACT, SpotType.LARGE],
  [VehicleType.TRUCK]: [SpotType.LARGE],
  [VehicleType.ELECTRIC]: [SpotType.EV_CHARGING]
};

export class ParkingFloor {
  constructor(floorNumber, spotConfigs) {
    this.floorNumber = floorNumber;
    this.spots = [];
    this.availableByType = new Map();

    Object.values(SpotType).forEach(type => {
      this.availableByType.set(type, new Set());
    });

    this.initializeSpots(spotConfigs);
  }

  initializeSpots(spotConfigs) {
    let spotId = 0;
    for (const { type, count } of spotConfigs) {
      for (let i = 0; i < count; i++) {
        spotId++;
        const id = `F${this.floorNumber}-S${spotId}`;
        const spot = SpotFactory.create(type, id, this.floorNumber);
        this.spots.push(spot);
        this.availableByType.get(type).add(spot);
      }
    }
  }

  findAvailableSpot(vehicleType) {
    const preferredTypes = VEHICLE_SPOT_PREFERENCE[vehicleType];
    if (!preferredTypes) return null;

    for (const spotType of preferredTypes) {
      const available = this.availableByType.get(spotType);
      if (available && available.size > 0) {
        const spot = available.values().next().value;
        return spot;
      }
    }
    return null;
  }

  assignSpot(spot, vehicle) {
    const success = spot.parkVehicle(vehicle, spot.version);
    if (success) {
      this.availableByType.get(spot.type).delete(spot);
    }
    return success;
  }

  releaseSpot(spot) {
    spot.vacate();
    this.availableByType.get(spot.type).add(spot);
  }

  getAvailableCount(spotType) {
    return this.availableByType.get(spotType)?.size || 0;
  }

  get totalAvailable() {
    let count = 0;
    for (const spots of this.availableByType.values()) {
      count += spots.size;
    }
    return count;
  }
}
import { Ticket } from '../models/Ticket.js';

export class ParkingLot {
  static #instance = null;

  constructor(pricingStrategy) {
    if (ParkingLot.#instance) {
      throw new Error('ParkingLot is a singleton. Use ParkingLot.getInstance()');
    }
    this.floors = [];
    this.activeTickets = new Map();
    this.pricingStrategy = pricingStrategy;
    this.observers = [];
  }

  static getInstance(pricingStrategy) {
    if (!ParkingLot.#instance) {
      ParkingLot.#instance = new ParkingLot(pricingStrategy);
    }
    return ParkingLot.#instance;
  }

  static resetInstance() {
    ParkingLot.#instance = null;
  }

  addFloor(floor) {
    this.floors.push(floor);
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  removeObserver(observer) {
    this.observers = this.observers.filter(o => o !== observer);
  }

  notifyObservers(floor, spotType, availableCount) {
    for (const observer of this.observers) {
      observer.onSpotUpdate(floor, spotType, availableCount);
    }
  }

  setPricingStrategy(strategy) {
    this.pricingStrategy = strategy;
    console.log(`[ParkingLot] Pricing strategy changed to: ${strategy.constructor.name}`);
  }

  enterVehicle(vehicle) {
    for (const floor of this.floors) {
      const spot = floor.findAvailableSpot(vehicle.type);
      if (!spot) continue;

      const currentVersion = spot.version;
      const success = floor.assignSpot(spot, vehicle);

      if (!success) {
        console.log(`[ParkingLot] Optimistic lock conflict on spot ${spot.id}. Retrying...`);
        return this.enterVehicle(vehicle);
      }

      const ticket = new Ticket(vehicle, spot);
      this.activeTickets.set(ticket.ticketId, ticket);

      this.notifyObservers(
        floor.floorNumber,
        spot.type,
        floor.getAvailableCount(spot.type)
      );

      console.log(
        `[ParkingLot] Vehicle ${vehicle.licensePlate} (${vehicle.type}) parked at spot ${spot.id}`
      );
      return ticket;
    }

    throw new Error(`No available spot for vehicle type: ${vehicle.type}`);
  }

  exitVehicle(ticketId) {
    const ticket = this.activeTickets.get(ticketId);
    if (!ticket) {
      throw new Error(`Invalid ticket: ${ticketId}`);
    }

    const durationMinutes = ticket.calculateDurationMinutes();
    const fee = this.pricingStrategy.calculateFee(ticket.vehicle.type, durationMinutes);

    const floor = this.floors.find(f => f.floorNumber === ticket.spot.floor);
    floor.releaseSpot(ticket.spot);

    ticket.close(fee);
    this.activeTickets.delete(ticketId);

    this.notifyObservers(
      floor.floorNumber,
      ticket.spot.type,
      floor.getAvailableCount(ticket.spot.type)
    );

    console.log(
      `[ParkingLot] Vehicle ${ticket.vehicle.licensePlate} exited. Duration: ${durationMinutes}min, Fee: $${fee}`
    );
    return ticket;
  }

  getAvailability() {
    const availability = {};
    for (const floor of this.floors) {
      availability[`Floor ${floor.floorNumber}`] = {};
      for (const [type, spots] of floor.availableByType) {
        if (spots.size > 0 || floor.spots.some(s => s.type === type)) {
          availability[`Floor ${floor.floorNumber}`][type] = spots.size;
        }
      }
    }
    return availability;
  }
}

Your services layer is complete. Next up, you'll wire everything together in main.js and run the full vehicle lifecycle: entry, spot assignment, fee calculation, exit, and a concurrency simulation with 5 simultaneous arrivals.

Wire It All Together and Run the Demo

You've built every layer of the parking lot system individually. Domain models define the structure, design patterns handle creation and behavior, and service classes manage coordination. Now it's time to bring them all together in a single entry point.

In this step, you'll replace the placeholder in main.js with a full integration demo. It initializes the Singleton lot, exercises every pattern in action, and simulates concurrent gate access to prove your optimistic locking works.

In this step, get ready to:
  • Initialize the ParkingLot singleton with floors, an observer, and a pricing strategy.
  • Run vehicles through the full entry and exit lifecycle with fee calculation.
  • Simulate concurrent gate access with 5 simultaneous entries via Promise.all.
Initialize the parking lot singleton

The ParkingLot facade is the single coordination point for the entire system. Before any vehicles can enter, it needs a pricing strategy injected, floors configured with spots, and an observer attached to report availability changes.

  • Open src/main.js in VS Code.
  • Replace the entire placeholder content with the following imports and initialization code:
import { VehicleType, SpotType } from './models/enums.js';
import { VehicleFactory } from './patterns/VehicleFactory.js';
import { ParkingFloor } from './services/ParkingFloor.js';
import { ParkingLot } from './services/ParkingLot.js';
import { DisplayBoard } from './patterns/Observer.js';
import {
  HourlyPricingStrategy,
  EVDiscountPricingStrategy,
  WeekendSurgePricingStrategy
} from './patterns/PricingStrategy.js';

console.log('=== PARKING LOT SYSTEM - HLD + LLD DEMO ===\n');

// --- Initialize the Parking Lot (Singleton + Factory + Observer) ---
ParkingLot.resetInstance();
const hourlyPricing = new HourlyPricingStrategy();
const lot = ParkingLot.getInstance(hourlyPricing);

const displayBoard = new DisplayBoard('Main Entrance');
lot.addObserver(displayBoard);

// Create floors using SpotFactory (via ParkingFloor)
const floor1 = new ParkingFloor(1, [
  { type: SpotType.MOTORCYCLE, count: 10 },
  { type: SpotType.COMPACT, count: 20 },
  { type: SpotType.LARGE, count: 5 }
]);

const floor2 = new ParkingFloor(2, [
  { type: SpotType.COMPACT, count: 30 },
  { type: SpotType.LARGE, count: 10 }
]);

const floor3 = new ParkingFloor(3, [
  { type: SpotType.LARGE, count: 20 },
  { type: SpotType.EV_CHARGING, count: 5 }
]);

lot.addFloor(floor1);
lot.addFloor(floor2);
lot.addFloor(floor3);

What does this code do?

  • ParkingLot.resetInstance() clears any existing singleton so the demo starts fresh every run.
  • HourlyPricingStrategy is injected via the Strategy pattern as the active pricing algorithm.
  • ParkingLot.getInstance(hourlyPricing) creates the singleton Facade that orchestrates all operations.
  • DisplayBoard is attached as an Observer. It logs availability changes to the console whenever a spot is assigned or released.
  • Three ParkingFloor instances configure the lot's physical layout: Floor 1 is mixed, Floor 2 is mid-size focused, and Floor 3 handles large vehicles plus EV charging.
  • Save src/main.js.

Seeing a red underline on the imports?

Make sure all your model, pattern, and service files exist in the correct folders. Each import path is relative to src/. Check that file names match exactly, including the .js extension in every import.

Why are my ES module imports not resolving?

Demonstrate the full vehicle lifecycle

This section exercises the Factory, Facade, and Strategy patterns together. The VehicleFactory creates typed vehicles, the ParkingLot facade handles entry and exit, and the pricing strategy calculates fees based on duration and vehicle type.

  • Add the following vehicle lifecycle code below the floor initialization in src/main.js:
// --- Full Vehicle Lifecycle (Factory + Facade) ---
console.log('\n--- VEHICLE ENTRY ---\n');

const car1 = VehicleFactory.create(VehicleType.CAR, 'ABC-1234');
const motorcycle1 = VehicleFactory.create(VehicleType.MOTORCYCLE, 'MTR-5678');
const truck1 = VehicleFactory.create(VehicleType.TRUCK, 'TRK-9012');
const ev1 = VehicleFactory.create(VehicleType.ELECTRIC, 'EV-3456');

const ticket1 = lot.enterVehicle(car1);
const ticket2 = lot.enterVehicle(motorcycle1);
const ticket3 = lot.enterVehicle(truck1);
const ticket4 = lot.enterVehicle(ev1);

// --- Check Availability ---
console.log('\n--- CURRENT AVAILABILITY ---\n');
console.log(JSON.stringify(lot.getAvailability(), null, 2));

// --- Vehicle Exit with Fee Calculation (Strategy) ---
console.log('\n--- VEHICLE EXIT (Hourly Pricing) ---\n');

const closedTicket1 = lot.exitVehicle(ticket1.ticketId);
const closedTicket2 = lot.exitVehicle(ticket3.ticketId);

// --- Strategy Swap at Runtime ---
console.log('\n--- STRATEGY SWAP: EV Discount Pricing ---\n');

const evDiscount = new EVDiscountPricingStrategy(hourlyPricing);
lot.setPricingStrategy(evDiscount);

const closedTicket3 = lot.exitVehicle(ticket4.ticketId);

What's happening here?

  • VehicleFactory.create() decouples vehicle instantiation from the demo logic. Adding a new vehicle type later requires no changes here.
  • lot.enterVehicle() is the Facade in action: it searches floors for a compatible spot, assigns it with optimistic locking, issues a ticket, and notifies all observers.
  • lot.exitVehicle() calculates the fee using whatever pricing strategy is currently active, then releases the spot back to the available pool.
  • The strategy swap to EVDiscountPricingStrategy wraps hourlyPricing as a decorator. It applies a 20% discount for electric vehicles while delegating to the base strategy for everything else.
  • Save src/main.js. You'll add one more section before running the demo.
Simulate concurrent gate access

In a real parking system, multiple gates process vehicles simultaneously. You need to prove that optimistic locking prevents two gates from assigning the same spot. While JavaScript's event loop is single-threaded, Promise.all simulates the pattern you'd see in a distributed multi-server deployment.

  • Add the concurrency simulation at the bottom of src/main.js:
// --- Concurrency Simulation ---
console.log('\n--- CONCURRENCY SIMULATION (5 simultaneous entries) ---\n');

const concurrentVehicles = [
  VehicleFactory.create(VehicleType.CAR, 'SIM-001'),
  VehicleFactory.create(VehicleType.CAR, 'SIM-002'),
  VehicleFactory.create(VehicleType.CAR, 'SIM-003'),
  VehicleFactory.create(VehicleType.CAR, 'SIM-004'),
  VehicleFactory.create(VehicleType.CAR, 'SIM-005')
];

const concurrentEntries = concurrentVehicles.map(vehicle =>
  Promise.resolve().then(() => lot.enterVehicle(vehicle))
);

Promise.all(concurrentEntries).then(tickets => {
  console.log(`\nAll ${tickets.length} vehicles parked successfully without conflicts.`);
  tickets.forEach(t => {
    console.log(`  ${t.vehicle.licensePlate} -> ${t.spot.id}`);
  });

  // --- Final Availability ---
  console.log('\n--- FINAL AVAILABILITY ---\n');
  console.log(JSON.stringify(lot.getAvailability(), null, 2));

  console.log('\n=== DEMO COMPLETE ===');
});

What does this code do?

  • Five cars are created and entered in parallel using Promise.resolve().then() to simulate async arrival from different gates.
  • Promise.all waits for all entries to resolve, then confirms each car received a unique spot.
  • If the optimistic lock version check ever fails (two gates try the same spot), enterVehicle retries automatically with the next available spot. No data corruption occurs.
  • Save src/main.js.
  • Run the full demo by executing this command in your terminal:
npm start

You should see output showing each vehicle parking at a specific spot (like F1-S1), the DisplayBoard logging availability changes for each floor, fee calculations on exit, the strategy swap announcement, and all 5 concurrent vehicles parking successfully with unique spot IDs.

Seeing an error when you run the demo?

  • If you see SyntaxError: Cannot use import statement outside a module, confirm your package.json has "type": "module" set.
  • If you see ERR_MODULE_NOT_FOUND, check that the file path in the error matches your actual folder structure. Every import must include the .js extension.
  • If you see ParkingLot is a singleton, make sure ParkingLot.resetInstance() is called before getInstance() at the top of your file.

Help me debug my parking lot demo output

Why does the fee show $20 for a 1-minute duration?

The HourlyPricingStrategy rounds up to the nearest hour using Math.ceil(durationMinutes / 60). Even 1 minute rounds up to 1 hour, so a car pays the hourly rate of $20. This is realistic. Real parking lots charge by the hour, not the minute.

✔️ Awesome, I've got everything!

Great. Double check you've saved src/main.js and that the demo runs cleanly with npm start.

ⓧ I'd like to double check the full code

import { VehicleType, SpotType } from './models/enums.js';
import { VehicleFactory } from './patterns/VehicleFactory.js';
import { ParkingFloor } from './services/ParkingFloor.js';
import { ParkingLot } from './services/ParkingLot.js';
import { DisplayBoard } from './patterns/Observer.js';
import {
  HourlyPricingStrategy,
  EVDiscountPricingStrategy,
  WeekendSurgePricingStrategy
} from './patterns/PricingStrategy.js';

console.log('=== PARKING LOT SYSTEM - HLD + LLD DEMO ===\n');

// --- Initialize the Parking Lot (Singleton + Factory + Observer) ---
ParkingLot.resetInstance();
const hourlyPricing = new HourlyPricingStrategy();
const lot = ParkingLot.getInstance(hourlyPricing);

const displayBoard = new DisplayBoard('Main Entrance');
lot.addObserver(displayBoard);

// Create floors using SpotFactory (via ParkingFloor)
const floor1 = new ParkingFloor(1, [
  { type: SpotType.MOTORCYCLE, count: 10 },
  { type: SpotType.COMPACT, count: 20 },
  { type: SpotType.LARGE, count: 5 }
]);

const floor2 = new ParkingFloor(2, [
  { type: SpotType.COMPACT, count: 30 },
  { type: SpotType.LARGE, count: 10 }
]);

const floor3 = new ParkingFloor(3, [
  { type: SpotType.LARGE, count: 20 },
  { type: SpotType.EV_CHARGING, count: 5 }
]);

lot.addFloor(floor1);
lot.addFloor(floor2);
lot.addFloor(floor3);

// --- Full Vehicle Lifecycle (Factory + Facade) ---
console.log('\n--- VEHICLE ENTRY ---\n');

const car1 = VehicleFactory.create(VehicleType.CAR, 'ABC-1234');
const motorcycle1 = VehicleFactory.create(VehicleType.MOTORCYCLE, 'MTR-5678');
const truck1 = VehicleFactory.create(VehicleType.TRUCK, 'TRK-9012');
const ev1 = VehicleFactory.create(VehicleType.ELECTRIC, 'EV-3456');

const ticket1 = lot.enterVehicle(car1);
const ticket2 = lot.enterVehicle(motorcycle1);
const ticket3 = lot.enterVehicle(truck1);
const ticket4 = lot.enterVehicle(ev1);

// --- Check Availability ---
console.log('\n--- CURRENT AVAILABILITY ---\n');
console.log(JSON.stringify(lot.getAvailability(), null, 2));

// --- Vehicle Exit with Fee Calculation (Strategy) ---
console.log('\n--- VEHICLE EXIT (Hourly Pricing) ---\n');

const closedTicket1 = lot.exitVehicle(ticket1.ticketId);
const closedTicket2 = lot.exitVehicle(ticket3.ticketId);

// --- Strategy Swap at Runtime ---
console.log('\n--- STRATEGY SWAP: EV Discount Pricing ---\n');

const evDiscount = new EVDiscountPricingStrategy(hourlyPricing);
lot.setPricingStrategy(evDiscount);

const closedTicket3 = lot.exitVehicle(ticket4.ticketId);

// --- Concurrency Simulation ---
console.log('\n--- CONCURRENCY SIMULATION (5 simultaneous entries) ---\n');

const concurrentVehicles = [
  VehicleFactory.create(VehicleType.CAR, 'SIM-001'),
  VehicleFactory.create(VehicleType.CAR, 'SIM-002'),
  VehicleFactory.create(VehicleType.CAR, 'SIM-003'),
  VehicleFactory.create(VehicleType.CAR, 'SIM-004'),
  VehicleFactory.create(VehicleType.CAR, 'SIM-005')
];

const concurrentEntries = concurrentVehicles.map(vehicle =>
  Promise.resolve().then(() => lot.enterVehicle(vehicle))
);

Promise.all(concurrentEntries).then(tickets => {
  console.log(`\nAll ${tickets.length} vehicles parked successfully without conflicts.`);
  tickets.forEach(t => {
    console.log(`  ${t.vehicle.licensePlate} -> ${t.spot.id}`);
  });

  // --- Final Availability ---
  console.log('\n--- FINAL AVAILABILITY ---\n');
  console.log(JSON.stringify(lot.getAvailability(), null, 2));

  console.log('\n=== DEMO COMPLETE ===');
});

Your parking lot system is fully operational. Every pattern is working in concert: the Singleton manages global state, the Factory creates objects without coupling, the Observer broadcasts changes, the Strategy swaps behavior at runtime, and the Facade keeps the API clean. Next up, you'll prove the system's extensibility by adding EV charging support without modifying a single existing class.

Prove Extensibility with EV and SOLID Review

Your parking lot system is fully functional with all five design patterns working together. The real test of good design is whether you can add new features without breaking existing code.

In this step, you'll review your codebase to prove that EV charging support was added without modifying any existing service classes. Then you'll map each SOLID principle to the specific design decisions that made this extension painless.

In this step, get ready to:
  • Verify that ElectricVehicle and EV_CHARGING were added without modifying existing classes.
  • Confirm that EVDiscountPricingStrategy works via runtime strategy swap with zero changes to existing strategies.
  • Document which SOLID principles made the extension possible.
Trace the EV vehicle extension through your codebase

The Open/Closed Principle says classes should be open for extension but closed for modification. You added EV support across the system. The question is: which files needed changes, and which stayed untouched?

  • Open src/models/Vehicle.js in VS Code.
  • Find the ElectricVehicle class at the bottom of the file.
export class ElectricVehicle extends Vehicle {
  constructor(licensePlate) {
    super(licensePlate, VehicleType.ELECTRIC);
  }
}

What makes this an extension, not a modification?

The Motorcycle, Car, and Truck classes remain exactly as they were. No existing constructor, method, or property was touched.

ElectricVehicle simply extends the same Vehicle base class and passes its own type to the constructor. The existing classes are completely unaware of it.

  • Open src/models/enums.js and look at the VehicleType and SpotType enums.
export const VehicleType = Object.freeze({
  MOTORCYCLE: 'MOTORCYCLE',
  CAR: 'CAR',
  TRUCK: 'TRUCK',
  ELECTRIC: 'ELECTRIC'
});

export const SpotType = Object.freeze({
  MOTORCYCLE: 'MOTORCYCLE',
  COMPACT: 'COMPACT',
  LARGE: 'LARGE',
  EV_CHARGING: 'EV_CHARGING'
});

Why are enum additions acceptable?

Adding ELECTRIC and EV_CHARGING to the frozen objects adds new entries but does not alter existing entries. Existing code that references VehicleType.CAR or SpotType.LARGE continues to resolve to the same values.

  • Now open src/models/ParkingSpot.js and look at the COMPATIBILITY map.
const COMPATIBILITY = {
  [SpotType.MOTORCYCLE]: [VehicleType.MOTORCYCLE],
  [SpotType.COMPACT]: [VehicleType.MOTORCYCLE, VehicleType.CAR],
  [SpotType.LARGE]: [VehicleType.MOTORCYCLE, VehicleType.CAR, VehicleType.TRUCK],
  [SpotType.EV_CHARGING]: [VehicleType.ELECTRIC]
};

Why didn't ParkingFloor or ParkingLot need changes?

ParkingFloor.findAvailableSpot() reads from the VEHICLE_SPOT_PREFERENCE map and delegates compatibility checks to ParkingSpot.canAccommodate(). Adding a new entry to that map is all that's needed for the floor to allocate EV spots correctly.

ParkingLot.enterVehicle() never checks vehicle types directly. It delegates entirely to floors. This means it handles ElectricVehicle without even knowing it exists.

  • Verify the demo still runs by running this command:
npm start

You should see the EV vehicle EV-3456 parked at an EV_CHARGING spot on Floor 3, and the strategy swap to EVDiscountPricingStrategy applying its 20% discount on exit.

Verify the pricing strategy extension

The Strategy pattern allows pricing algorithms to be swapped at runtime. Adding a new strategy should never require touching existing ones.

  • Open src/patterns/PricingStrategy.js and scroll to the EVDiscountPricingStrategy class at the bottom.
export class EVDiscountPricingStrategy extends PricingStrategy {
  constructor(baseStrategy) {
    super();
    this.baseStrategy = baseStrategy;
    this.discountRate = 0.8;
  }

  calculateFee(vehicleType, durationMinutes) {
    const baseFee = this.baseStrategy.calculateFee(vehicleType, durationMinutes);
    if (vehicleType === VehicleType.ELECTRIC) {
      return baseFee * this.discountRate;
    }
    return baseFee;
  }
}

What does this strategy demonstrate?

  • EVDiscountPricingStrategy wraps any base strategy via its constructor, applying the Decorator pattern on top of Strategy.
  • HourlyPricingStrategy, FlatRatePricingStrategy, and WeekendSurgePricingStrategy are completely unmodified. No existing calculateFee() method was touched.
  • ParkingLot calls this.pricingStrategy.calculateFee() without caring which concrete class is behind the abstraction.
  • Look at how main.js swaps the strategy at runtime.
const evDiscount = new EVDiscountPricingStrategy(hourlyPricing);
lot.setPricingStrategy(evDiscount);

One line creates the new strategy wrapping the existing hourly pricing. One line injects it. The lot starts using EV discount pricing immediately without restarting or reconfiguring anything.

Map each SOLID principle to your design decisions

Now that you've traced the extension, you can formally connect each SOLID principle to a specific part of your codebase.

  • Review the five principles below and match them to what you built.

SRP: Single Responsibility Principle

Each class has one reason to change.

  • ParkingSpot only manages its own occupancy state and compatibility check.
  • ParkingFloor only manages spot allocation within one floor.
  • HourlyPricingStrategy only calculates fees. It does not park vehicles or send notifications.

OCP: Open/Closed Principle

Open for extension, closed for modification.

  • ElectricVehicle was added by creating a new class, not by editing Motorcycle, Car, or Truck.
  • EVDiscountPricingStrategy was added by extending PricingStrategy, not by adding an if-else to HourlyPricingStrategy.
  • ParkingFloor and ParkingLot remained completely unmodified despite the new vehicle and spot types.

LSP: Liskov Substitution Principle

Subclasses can replace their parent without breaking behavior.

  • ElectricVehicle honors the Vehicle contract. Anywhere the system expects a Vehicle (in Ticket, in parkVehicle()), an ElectricVehicle works identically.
  • EVDiscountPricingStrategy implements calculateFee(vehicleType, durationMinutes) with the same signature and return type. The ParkingLot can swap it in without any special handling.

ISP: Interface Segregation Principle

Clients should not be forced to depend on methods they do not use.

  • PricingStrategy has a single method: calculateFee(). No implementer is forced to provide irrelevant methods like parkVehicle() or notifyObservers().
  • ParkingObserver has a single method: onSpotUpdate(). Observers only implement what they need to react to.

DIP: Dependency Inversion Principle

High-level modules depend on abstractions, not concretions.

  • ParkingLot depends on the PricingStrategy abstraction, not on HourlyPricingStrategy directly. This is why setPricingStrategy() works with any strategy you pass in.
  • ParkingLot depends on the ParkingObserver abstraction. It can notify a DisplayBoard, a mobile app notifier, or any future observer without code changes.

The interview-ready summary

If asked "how would you add EV support?" in a system design interview, the answer is: "I wouldn't modify anything. I'd add an ElectricVehicle subclass, add an EV_CHARGING spot type with its compatibility rule, add a new pricing strategy, and register them in the factories. The service layer stays untouched because it depends on abstractions."

Your system now has a provable extensibility story. You added an entire vehicle category with custom pricing and dedicated infrastructure, and the core orchestration code never changed. This is the hallmark of a well-designed system.

Secret mission

Add a Reservation System with Time-Slot Booking and Automatic Expiry

Your parking lot works great for walk-ins. But what about vehicles that want to guarantee a spot before they arrive? Build a reservation system that introduces time-aware availability without modifying any existing class.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files for future reference, or delete them entirely.

Resources you used:

  • Project directory (parking-lot-system/) with all source files and design docs.
  • Node.js runtime (pre-existing on your machine).

Keep everything running

No action needed. Choose this if you want to keep the project as a reference for system design interviews or future implementations.

  • Your project files remain on your Desktop in the parking-lot-system folder.
  • You can re-run the demo anytime with node src/main.js.

Pause - I'll come back to this later

Since this is a local project with no running processes, there is nothing to pause. Your files will stay on disk until you decide to delete them.

  • No background services are running from this project.
  • Your files use minimal disk space and have no ongoing impact.

Delete - I don't want to use this again

Remove all project files from your machine. This cannot be undone.

  • Delete the project directory by running this command (macOS/Linux):
rm -rf ~/Desktop/parking-lot-system
  • On Windows, run this command instead:
rmdir /s /q %USERPROFILE%\Desktop\parking-lot-system
  • Node.js was already installed on your machine before this project. No need to remove it.

Nice Work!

Nice Work!

You've designed and built a parking lot system from the ground up, covering every layer from high-level architecture to working object-oriented code. This is the kind of artifact you could walk through in a system design interview or use as a reference architecture.

You've learned how to:

  • Produce a complete High Level Design document with system architecture, REST API contracts, relational database schema, and capacity estimation.
  • Apply 5 design patterns purposefully: Strategy (pricing), Factory (object creation), Singleton (lot instance), Observer (availability notifications), and Facade (orchestration).
  • Demonstrate SOLID principles with an extensibility proof that added EV charging support and discount pricing without modifying a single existing class.
  • Built a reservation system with time-slot booking, time-aware availability checking, and automatic expiry.

Ready to quiz yourself?