Beginner28 min readMachine Coding Practicelive prototype

Parking Lot

The single most-asked machine-coding problem. You get one line — “design a parking lot” — and 60 minutes to hand back code that runs. This lesson builds the whole system with you: the questions you ask, the classes you write, the two flows (park and unpark), the race condition nobody sees coming, and the four follow-ups the interviewer always asks next.

The idea

What it is

The interviewer says one sentence — “Design a parking lot.” — and starts a timer. That is the whole prompt. It is deliberately tiny, because the question is not really “can you park a car?”. It is “when I hand you a vague problem, what do you do in the first ten minutes?”

The good news: a parking lot is a system you already understand. You have driven into one. You took a ticket, you parked, you paid on the way out. Everything you need to model is a thing you can point at in the real world — and that is exactly why this problem is the warm-up everybody starts with.

ParkingLot Floor 2 S S M M L L Floor 1 S S M M L L ParkingFloor ParkingSpot ENTRY Vehicle EXIT T-7 spot F1-03 in 09:00 Ticket
Every orange word is a class you are about to write. That is not a coincidence — the fastest way into this problem is to name the things you can already see.

The whole system in three sentences

A ParkingLot owns floors, and floors own spots. park(vehicle) finds a free spot the vehicle fits into, marks it taken, and hands back a Ticket stamped with the entry time. unpark(ticket) reads that timestamp, charges for the time, and frees the spot. Everything else in this lesson is detail hanging off those three sentences.

What is actually being graded

Interviewers do not score you on how many classes you produced. Almost every scorecard for this round looks like the same five lines:

  1. Does it run? A main() that parks three vehicles and prints two bills beats a beautiful half-finished class hierarchy. Every time.
  2. Are the names the real-world names? ParkingSpot, Ticket, Vehicle. Not SpotDataManager, not TicketVO.
  3. Can I change a rule without editing your code? This is the big one. They will say “now charge differently on weekends”, and they are watching whether you open an existing method or add a new class.
  4. Did you handle the ugly cases? Lot full. Truck when only bike spots are free. Ticket presented twice. Two cars arriving at once.
  5. Did you talk while you worked? Silence reads as “stuck”, even when you are not.

This lesson is the worked example of the framework

The five steps come from A repeatable 5-step frameworkclarify → entities → API → class diagram → code. Here we actually walk them, end to end, on a real problem. If a step feels rushed, that lesson explains the why; this one shows the what.

Mechanics

How it works

Step 1 · Clarify — 5 minutes, and it is the cheapest time you will spend

You cannot build “a parking lot”. You can build this parking lot. So shrink the problem until it has edges. Four or five short questions is enough — and you say the answers out loud so they become an agreement, not a guess.

  • How big is it?“Two floors, about 16 spots?” If they say “assume small”, a plain in-memory list is fine and you just saved yourself an hour of indexing.
  • What can drive in?“Bikes, cars, trucks?” Three types means three sizes, which means a spot has a size too.
  • How is it charged?“Per hour, paid at exit?” Now you know the ticket needs an entry timestamp. That one answer designs a whole class.
  • What happens when it is full?“Turn the vehicle away.” An answer here stops you from crashing on the demo.
  • What is not in scope? — payments, login, a UI, a database. The most valuable question in the round is the one that removes work.
IN SCOPE — I will build this 2 floors · 16 spots Bike · Car · Truck park() and unpark() fee per started hour in-memory, one lot a main() demo OUT — agreed, not built payment gateway login / accounts web API or UI database / persistence reservations monthly passes
Write this board down where the interviewer can see it. The right-hand column is what you point at, calmly, when you decide not to build something at minute 45.

The two questions that eat your clock

“Which database?” and “Should there be a UI?” In a 60-minute round the answers are always in-memory and a main() method. State those as assumptions in one sentence and move. Every minute in a rabbit hole is a minute not spent on code that runs.

Step 2 · Nouns become classes, verbs become methods

Read your clarified problem back and underline the nouns. A noun that has its own data and its own behaviour is a class. Then underline the verbs — those are the methods, and they nearly always belong to the noun that owns the data they touch.

NOUN IN THE PROBLEM CLASS YOU WRITE “a parking lot” “floors” “spots” “a vehicle” “a ticket” “charged per hour” ParkingLot ParkingFloor ParkingSpot Vehicle← Bike · Car · Truck Ticket PricingStrategy VERBS → METHODS park() unpark() fee() isFree() fits()
Notice the last row. “Charged per hour” is not a noun you can point at — it is a rule. Rules that might change become their own object. That single move is what makes this design extensible later.
  • ParkingLot — the front door of the system. It owns the floors and it is the only class the outside world talks to.
  • ParkingFloor — a list of spots. In a small lot you can skip this and let the lot hold spots directly; say so out loud, and add it back the moment they mention multiple floors.
  • ParkingSpot — has an id, a size, and either a vehicle in it or nothing. It knows two things: am I free? and does this vehicle fit?
  • Vehicle — abstract, with Bike, Car, Truck under it. Each one answers what size spot do I need?
  • Ticket — the contract between the lot and the driver: which vehicle, which spot, entry time. Once created, nothing on it changes except its status.
  • SpotAllocationStrategy and PricingStrategy — the two rules that the interviewer is most likely to change on you. Interfaces, not if statements.

Use enums for the fixed vocabularies

VehicleType { BIKE, CAR, TRUCK }, SpotSize { SMALL, MEDIUM, LARGE }, TicketStatus { ACTIVE, PAID, LOST }. Three enums, thirty seconds, and every stringly-typed bug in this problem disappears. More on why in Enums & constants.

Step 3 · Write the API before you write the code

Before a single field, write the method signatures a caller would use. Names, parameters, return types — nothing else. If you can describe the whole system in four lines here, your model is right. If you cannot, you are still confused and it is far cheaper to find that out now.

the public API
lot.park(vehicle)              -> Ticket        // throws when the lot is full
lot.unpark(ticketId, exitAt)   -> Money         // the bill; frees the spot
lot.availableSpots(size)       -> int           // for the display board
lot.setPricing(strategy)       -> void          // swap the rule, not the code

Three things worth defending out loud, because interviewers ask about all three:

  • park returns a Ticket, not a boolean. The caller needs something to bring back later. A boolean tells the driver nothing.
  • unpark takes a ticket id, not the ticket object. A real gate scans a barcode. Taking an id also means the lot decides whether that ticket is still open — the caller cannot lie to it.
  • Time is a parameter, not now() read inside. unpark(id, exitAt) is testable in one line. unpark(id) that calls the clock internally is not — you would have to sleep for an hour to test a two-hour bill. This is the single easiest senior-signal to give in this round.

Money is not a double

0.1 + 0.2 is not 0.3 in floating point, and a parking bill that is off by a paisa is a bug you will not enjoy explaining. Use a whole-number minor unit (paise/cents) as long, or BigDecimal. Saying this out loud costs five seconds and lands well. See Immutability & value objects for why Money deserves to be its own tiny type.

Step 4 · The class diagram — draw it, do not describe it

Two small diagrams beat one crowded one. The first is the core model: what holds what. Read the diamonds as “owns” — a lot owns its floors, a floor owns its spots, and if the lot is demolished they go with it.

ParkingLot + park(v) : Ticket + unpark(id, t) : Money 1..* ParkingFloor - spots : List<Spot> 1..* ParkingSpot - size : SpotSize + fits(v) : boolean + tryAssign(v) : boolean Vehicle «abstract» - plate : String + requiredSize() : Size 0..1 Bike Car Truck Ticket - id : String - entryAt : Instant - status : Status issues
Filled diamond = composition (“owns, and dies with”). Open arrow = a plain reference. Hollow triangle = inheritance. The one field worth pointing at is entryAt — the entire billing system is downstream of that timestamp. Notation refresher: Class diagrams.

The second diagram is the part that wins the round. Both rules the interviewer is likely to change — which spot do we give out? and how much do we charge? — hang off the lot as interfaces, with the real rules as small classes underneath.

ParkingLot holds one of each ↓ uses uses «interface» SpotAllocationStrategy + findSpot(spots, v) : Spot «interface» PricingStrategy + fee(ticket, exitAt) : Money NearestFirst SpreadOut HourlyRate SlabRate WeekendRate — the follow-up. 1 new file, 0 edits.
This is the Strategy pattern, and it is the answer to “now charge differently on weekends”. You add a class. You do not open unpark(). That is Open/Closed (OCP) made concrete — and in the prototype above, the 🎯/📊 and ⏱/🪜 chips are exactly these four boxes being swapped at runtime.

Step 5 · The two flows, message by message

There are only two stories in this system. Walk both out loud while you draw them — this is where an interviewer decides whether you actually understand your own diagram.

EntryGate ParkingLot «strategy» Spot F1-03 Ticket park(car) findSpot(spots, car) smallest that fits → F1-03 tryAssign(car) true — it was still free new Ticket(car, F1-03, 09:00) Ticket T-7
park() — five messages, and only the lot talks to more than one collaborator. If a class in your diagram is not on this line, ask yourself whether it earns its place. Notation: Sequence diagrams.
ExitGate ParkingLot «pricing» Spot F1-03 Ticket unpark(“T-7”, 12:00) fee(ticket, 12:00) ₹65 (3 h, slab rate) release() close(PAID) ₹65
unpark() — price it, free the spot, close the ticket, hand back the amount. Order matters: close the ticket before you release the spot in your head, so a ticket can never be billed twice.

Choosing a spot: the rule everyone gets half-right

A bike fits in a bike spot. It also physically fits in a truck spot. So the naive rule — “find any free spot big enough” — is correct and quietly terrible: park three bikes and your two truck spots are gone. The rule you want is the smallest free spot that fits.

SMALL MEDIUM LARGE Bike ✓ take Car ✓ take Truck ✓ take green = it physically fits · orange = the one you should actually hand out
Two lines of code: spot.size >= vehicle.requiredSize to filter, then take the minimum size among what is left. In the prototype, the blue spots are the green cells and the orange spots are the orange cell.

Once the size filter has run you still have a choice among equals, and that choice is the strategy. Two that are easy to defend:

  • Nearest first — lowest floor, lowest spot number. Shortest walk for the driver. Floor 1 fills completely before Floor 2 gets a car.
  • Spread out — send each arrival to the emptiest floor. Slower ramps stay clear, and no single floor jams at rush hour.
  • Cheapest / reserved / EV-only — the same interface, more classes. This is why findSpot takes the spot list as a parameter instead of reaching inside the lot: the strategy stays a small, testable, stateless object.

The real reason to use a strategy here

It is not that you expect to ship SpreadOut. It is that when the interviewer says “what if we wanted to fill the top floor first?”, your answer is “one new class, nothing else changes” — and you can say it in four seconds instead of scrolling through an if/else chain looking for where to wedge it in.

Pricing: the same ticket, two different bills

The fee is a pure function of the ticket and the exit time. Nothing else. Keep it that way and pricing becomes trivially testable — and the interviewer's favourite follow-up becomes a one-liner.

same ticket · Car · in 09:00 · out 12:00 · 3 h HourlyRate rate for CAR₹20 / h started hours3 3 × 20 TOTAL₹60 SlabRate first 2 h₹50 each extra hour₹15 50 + 1 × 15 TOTAL₹65
unpark() never knew which of these ran. It called pricing.fee(ticket, exitAt) and printed the number. Toggle ⏱ Flat / 🪜 Slabs in the prototype and watch the open tickets re-price live.
  • Round up to a started hour. 61 minutes is 2 hours. Say this out loud — it is the kind of detail that separates “I thought about it” from “I divided by 3600”.
  • Charge a minimum. Somebody who leaves after 4 minutes still pays for one hour, or your lot is free for anyone doing a drop-off.
  • Never bill from the current clock. Bill from ticket.entryAt to the exitAt you were handed. That is what makes the whole thing unit-testable.

The ticket has a life, and it can only move forwards

A ticket is issued, then paid, then closed. It never goes backwards. Modelling that as an explicit status — instead of inferring it from “is the spot empty?” — is what stops a ticket being billed twice.

ACTIVE unpark(id, exitAt) PAID spot released CLOSED ticket lost → flat penalty, then the same path out
Two lines of guard code buy you this: unpark on a ticket that is not ACTIVE throws. Notation: State diagrams.

Two cars, one spot — the bug nobody mentions until they do

A parking lot has several entry gates, and gates do not take turns. Two threads can read “F1-03 is free” in the same instant, and both can then write themselves into it. You just gave one spot to two cars, and the second one is going to be upset.

UNGUARDED — both gates read “free” before either writes Gate A read F1-03 → free write car A Gate B read F1-03 → free write car B ✗ one spot, two cars GUARDED — “check and take” happens as one indivisible step Gate A tryAssign(F1-03) → true Gate B tryAssign(F1-03) → false retry → gets F1-04 ✓ every car parked
The fix is not “lock the whole lot”. It is to make check-and-take one indivisible operation on the spot, and let a loser simply ask again.
the whole fix
// ParkingSpot — the check and the take cannot be split apart
public synchronized boolean tryAssign(Vehicle v) {
    if (vehicle != null) return false;   // someone got here first
    vehicle = v;
    return true;
}

// ParkingLot — a loser just asks the strategy again
public Ticket park(Vehicle v) {
    for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
        ParkingSpot spot = allocation.findSpot(spots, v)
            .orElseThrow(() -> new LotFullException(v.getType()));
        if (spot.tryAssign(v)) return issueTicket(v, spot);
    }
    throw new LotFullException(v.getType());   // heavy contention, give up cleanly
}

Do not put synchronized on park() and call it done

It is correct, and it means one car enters the lot at a time across every gate in the building. Lock the spot, not the lot: contention drops to the handful of cars actually competing for the same space. If they push further, mention an AtomicReference<Vehicle> with compare-and-set — same idea, no lock at all. Background: Locks, Mutex, Semaphore, Atomic operations & CAS and Deadlock, race conditions, starvation.

How much of this to actually build

In a 60-minute round, write tryAssign as synchronized and say the rest out loud. That is four extra characters of code and a 20-second explanation, and it reliably reads as senior. Building a full lock-free allocator is how you run out of time.

The extensibility test — where they will actually push you

With ten minutes left, the interviewer stops asking about parking and starts asking about change. This is the real exam. Each of these should be answerable in one sentence, and the sentence should contain the words “a new class”.

“NOW ALSO…” WHAT IT COSTS YOU charge more on weekends +1 class 0 edits fill the top floor first +1 class 0 edits add an EV charging spot +1 enum, +1 class 1 edit open a second entry gate 0 classes make it safe
The first two are free because you made the rules into interfaces in step 4. That is the entire return on those ten minutes of diagramming.
  • “Charge differently on weekends.”class WeekendRate implements PricingStrategy. Wire it in main(). Nothing else is touched.
  • “Fill the top floor first.”class TopFloorFirst implements SpotAllocationStrategy. Same story.
  • “Add electric-vehicle spots with a charger.” → a new SpotFeature (or an ELECTRIC spot type) plus an EvOnly strategy. One edit, because the size-fit rule now has a second dimension — say that honestly rather than pretending it is free.
  • “Two entrance gates.” → no new classes; this is the concurrency answer above. tryAssign is already atomic, so you are done.
  • “Show free spots on a board at the entrance.” → a DisplayBoard that the lot notifies on every park and unpark. That is Observer, and mentioning it by name costs you nothing.
  • “Monthly pass holders skip the ticket.” → a ParkingPass and a second entry path. Be careful here: this is the one follow-up big enough to change your model, so scope it out loud before you touch anything.

Spending the 60 minutes

60 MINUTES 5m 8m 7m 8m 32m clarify entities API diagram code + demo minute 45 — stop adding, start making it run
The blue block is only 28 minutes, and every minute of it removes a decision from the orange block. That is why planning feels slower and finishes faster.
  1. Enums and Vehicle first (3 min) — tiny, unblocks everything else.
  2. ParkingSpot with fits and tryAssign (5 min) — the heart of the model.
  3. ParkingLot.park() with a hardcoded nearest-first strategy (8 min) — get a ticket printing before you make anything pluggable.
  4. Ticket + unpark() with the simplest pricing (8 min) — now the loop is closed and you have a working system.
  5. Extract the two strategies (5 min) — only now. Extraction is a five-minute refactor when the thing works, and a rabbit hole when it does not.
  6. main() demo (5 min) — park a bike, a car and a truck, print two bills, try to park into a full lot. Print it. Run it.

At minute 45, freeze the feature list

Whatever is unbuilt at 45 stays unbuilt. Spend the last quarter making the demo run and printing clean output. A working lot with one pricing rule scores far above a half-typed lot with four.

Five ways this round is usually lost

  • Typing at minute zero. Twenty minutes later you discover the ticket needed an entry time and the rewrite eats the round.
  • A god ParkingLot that finds spots, prices tickets, prints receipts and manages gates. Ask of each method: is this the lot's job? See Single Responsibility (SRP).
  • Anaemic classes. ParkingSpot with nothing but getters and setters, while ParkingLot reaches in and mutates it. Behaviour belongs next to the data it touches — that is Tell, Don't Ask.
  • Pattern cosplay. A ParkingLotFactoryProviderImpl before there is a single working park(). Two strategies is the right amount of pattern for this problem; see Pattern overuse & anti-patterns.
  • No demo. If it does not run, most interviewers cannot give you the top band no matter how pretty the diagram was.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

A working parking lot, not a slideshow. Pick a vehicle and press ▶ Park — the lot answers in four beats: the vehicle arrives, every spot that fits lights up blue, the smallest fitting size narrows it down to orange, and the strategy picks one. A ticket drops into the tray with the entry time on it. The mono line at the top always shows the real method call behind what you just did. Then change the rules: 🎯 Nearest vs 📊 Spread re-routes cars without touching park(), ⏱ Flat vs 🪜 Slabs re-prices without touching unpark(). Push ⏩ +1 hour and watch every open bill grow. Push ⚡ Fill the lot, then Park — and watch the barrier stay down.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Watch the four beats of one park

Pick 🚗 Car and press ▶ Park. Do not skip the middle: beat 2 lights up every spot big enough in blue, beat 3 narrows to the smallest fitting size in orange, and only then does the strategy pick one. Now do the same with 🏍 Bike — notice how many more spots turn blue, and that it still takes an S.

try 02

Change the strategy, not the code

Park four cars on 🎯 Nearest and watch Floor 1 fill left to right. Press ↺ Reset, switch to 📊 Spread, park four cars again — they alternate floors. Read the mono line at the top while you do it: park() is called identically both times. The only thing that changed is which object the lot is holding.

try 03

Make the clock cost money

Park a car, then press ⏩ +1 hour three times. The open ticket re-prices itself on every press. Now switch ⏱ Flat → 🪜 Slabs and watch the same ticket land on a different number — ₹60 becomes ₹65 — with nothing else touched. That is one interface with two implementations.

try 04

Fill it up and get refused

Press ⚡ Fill the lot, then select 🚚 Truck and press ▶ Park. The barrier stays down and the entrance shakes. Ask yourself what your code should do here: return null, return an Optional, or throw? Say your answer out loud — the interviewer will ask.

try 05

Find the smallest-fit trade-off

Reset, then park three trucks first, then try a car and a bike. Compare with the reverse order. Smallest-fit protects the big spots, but it cannot save you once trucks have taken them. This is the exact conversation to have when they ask “what if the lot is 80% full?”

try 06

Now build it yourself

Open a blank file and write it from memory in this order: enums → VehicleParkingSpotpark()Ticketunpark()main(). Time yourself. Then add WeekendRate implements PricingStrategy and confirm you changed zero existing lines to wire it in. If you had to edit something, your interface is in the wrong place.

In practice

When to use it — and what trips people up

The follow-ups, and a one-sentence answer for each

You will be asked at least three of these. Rehearse the answers until they are boring — the calm, fast answer is the one that scores.

  • “How do you handle 5,000 spots?” — Keep a free-list per (floor, size) instead of scanning every spot. Allocation drops from O(n) to O(1). Say it; only build it if there is time.
  • “Multiple entry and exit gates?” — Gates are just callers of the same lot. The interesting part is concurrency, and tryAssign already handles it.
  • “What if someone loses their ticket?”TicketStatus.LOST plus a flat penalty rate. It is a new PricingStrategy, which is a nice thing to be able to say.
  • “Reserve a spot in advance?” — Reservation is a third state on the spot (FREE / RESERVED / OCCUPIED) with an expiry. Warn them this changes the model — the spot is no longer a simple free/taken boolean.
  • “Different rates per floor?” — The pricing strategy already receives the ticket, and the ticket knows its spot, which knows its floor. Zero model change.
  • “Would you persist this?” — A ParkingLotRepository behind an interface, in-memory today, SQL tomorrow. See Repository — naming it takes four seconds and shows you know where the seam goes.

Where this design would genuinely stop working

  • Many lots in many cities. The single in-memory ParkingLot becomes a service per lot with its own store. That is a high-level design conversation, not this round.
  • Real money. Payments need idempotency, retries and a reconciliation trail. The moment payments are in scope, unpark returning a long is not enough.
  • Sensors instead of gates. If occupancy comes from hardware events rather than park() calls, the lot becomes an event consumer and the ticket stops being the source of truth. Different problem; see Pub/Sub & Event-driven.

If you only remember one thing

Get parkTicketunpark → bill running end-to-end before you make anything pluggable. A working system that you then refactor for ten minutes always beats a perfectly abstract system that never printed a line.

What it gives you

  • Every class maps to something you can point at in a real car park — the design explains itself with no glossary.
  • The two rules most likely to change (spot choice, pricing) are interfaces, so the classic follow-ups cost one new class and zero edits.
  • unpark(id, exitAt) takes time as a parameter, so the entire billing system is unit-testable without waiting or mocking a clock.
  • tryAssign makes check-and-take atomic on the spot, so multiple gates work without serialising the whole lot.
  • Small enough to actually finish in 60 minutes, with a main() that prints real output.

Common mistakes

  • Allocation scans every spot — fine for hundreds, wrong for tens of thousands without a per-size free-list.
  • ParkingFloor is close to a pass-through in a small lot; keeping it can read as ceremony until multi-floor rules actually exist.
  • Everything is in memory. A restart forgets every open ticket, so real deployments need persistence this design does not have.
  • Two strategy interfaces is the right amount of pattern here — but the same instinct, applied to every class, is exactly how this problem gets over-engineered.
  • Vehicle-size and spot-size as one ordered scale breaks down the moment a spot has features (EV charger, handicapped, valet-only) as well as a size.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

import java.time.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

// ---------- vocabulary ----------
enum VehicleType { BIKE, CAR, TRUCK }
enum SpotSize    { SMALL, MEDIUM, LARGE }        // ordinal order == physical order
enum TicketStatus { ACTIVE, PAID, LOST }

// ---------- vehicles ----------
abstract class Vehicle {
    private final String plate;
    private final VehicleType type;
    protected Vehicle(String plate, VehicleType type) { this.plate = plate; this.type = type; }
    public String plate()      { return plate; }
    public VehicleType type()  { return type; }
    public abstract SpotSize requiredSize();
}
class Bike  extends Vehicle { public Bike(String p)  { super(p, VehicleType.BIKE);  } public SpotSize requiredSize() { return SpotSize.SMALL;  } }
class Car   extends Vehicle { public Car(String p)   { super(p, VehicleType.CAR);   } public SpotSize requiredSize() { return SpotSize.MEDIUM; } }
class Truck extends Vehicle { public Truck(String p) { super(p, VehicleType.TRUCK); } public SpotSize requiredSize() { return SpotSize.LARGE;  } }

// ---------- spot: knows how to be taken safely ----------
class ParkingSpot {
    private final String id;
    private final int floor;
    private final SpotSize size;
    private Vehicle vehicle;                       // null == free

    ParkingSpot(String id, int floor, SpotSize size) { this.id = id; this.floor = floor; this.size = size; }

    public String id()     { return id; }
    public int floor()     { return floor; }
    public SpotSize size() { return size; }

    public boolean fits(Vehicle v) { return size.ordinal() >= v.requiredSize().ordinal(); }
    public synchronized boolean isFree() { return vehicle == null; }

    /** Check and take, indivisibly. Two gates cannot both win. */
    public synchronized boolean tryAssign(Vehicle v) {
        if (vehicle != null) return false;
        vehicle = v;
        return true;
    }
    public synchronized void release() { vehicle = null; }
}

// ---------- ticket: issued once, then only its status moves ----------
class Ticket {
    private final String id;
    private final Vehicle vehicle;
    private final ParkingSpot spot;
    private final Instant entryAt;
    private TicketStatus status = TicketStatus.ACTIVE;

    Ticket(String id, Vehicle v, ParkingSpot s, Instant entryAt) {
        this.id = id; this.vehicle = v; this.spot = s; this.entryAt = entryAt;
    }
    public String id()          { return id; }
    public Vehicle vehicle()    { return vehicle; }
    public ParkingSpot spot()   { return spot; }
    public Instant entryAt()    { return entryAt; }
    public TicketStatus status(){ return status; }
    public void markPaid()      { this.status = TicketStatus.PAID; }
}

// ---------- rule 1: which spot? ----------
interface SpotAllocationStrategy {
    Optional<ParkingSpot> findSpot(List<ParkingSpot> spots, Vehicle v);

    /** Shared by every strategy: free, big enough, and the SMALLEST such size. */
    default List<ParkingSpot> candidates(List<ParkingSpot> spots, Vehicle v) {
        List<ParkingSpot> fits = spots.stream()
                .filter(ParkingSpot::isFree).filter(s -> s.fits(v)).toList();
        return fits.stream()
                .min(Comparator.comparingInt(s -> s.size().ordinal()))
                .map(best -> fits.stream().filter(s -> s.size() == best.size()).toList())
                .orElse(List.of());
    }
}

class NearestFirst implements SpotAllocationStrategy {
    public Optional<ParkingSpot> findSpot(List<ParkingSpot> spots, Vehicle v) {
        return candidates(spots, v).stream()
                .min(Comparator.comparingInt(ParkingSpot::floor)
                               .thenComparing(ParkingSpot::id));
    }
}

class SpreadOut implements SpotAllocationStrategy {
    public Optional<ParkingSpot> findSpot(List<ParkingSpot> spots, Vehicle v) {
        Map<Integer, Long> freePerFloor = new HashMap<>();
        for (ParkingSpot s : spots)
            if (s.isFree()) freePerFloor.merge(s.floor(), 1L, Long::sum);
        return candidates(spots, v).stream()
                .max(Comparator.comparingLong((ParkingSpot s) -> freePerFloor.getOrDefault(s.floor(), 0L))
                               .thenComparing(s -> s.id(), Comparator.reverseOrder()));
    }
}

// ---------- rule 2: how much? ----------
interface PricingStrategy {
    /** Money in paise/cents. Never a double. */
    long fee(Ticket t, Instant exitAt);

    default long startedHours(Ticket t, Instant exitAt) {
        long minutes = Duration.between(t.entryAt(), exitAt).toMinutes();
        return Math.max(1, (long) Math.ceil(minutes / 60.0));   // 61 min == 2 h, and never zero
    }
}

class HourlyRate implements PricingStrategy {
    private static final Map<VehicleType, Long> RATE =
            Map.of(VehicleType.BIKE, 1000L, VehicleType.CAR, 2000L, VehicleType.TRUCK, 3000L);
    public long fee(Ticket t, Instant exitAt) {
        return startedHours(t, exitAt) * RATE.get(t.vehicle().type());
    }
}

class SlabRate implements PricingStrategy {
    public long fee(Ticket t, Instant exitAt) {
        long h = startedHours(t, exitAt);
        return h <= 2 ? 5000L : 5000L + (h - 2) * 1500L;
    }
}

class LotFullException extends RuntimeException {
    LotFullException(VehicleType t) { super("No free spot for a " + t); }
}

// ---------- the front door ----------
class ParkingLot {
    private static final int MAX_RETRIES = 3;

    private final List<ParkingSpot> spots;
    private final Map<String, Ticket> open = new ConcurrentHashMap<>();
    private final AtomicInteger seq = new AtomicInteger();
    private SpotAllocationStrategy allocation;
    private PricingStrategy pricing;

    ParkingLot(List<ParkingSpot> spots, SpotAllocationStrategy a, PricingStrategy p) {
        this.spots = List.copyOf(spots); this.allocation = a; this.pricing = p;
    }
    public void setAllocation(SpotAllocationStrategy a) { this.allocation = a; }
    public void setPricing(PricingStrategy p)           { this.pricing = p; }

    public Ticket park(Vehicle v, Instant now) {
        for (int i = 0; i < MAX_RETRIES; i++) {
            ParkingSpot spot = allocation.findSpot(spots, v)
                    .orElseThrow(() -> new LotFullException(v.type()));
            if (spot.tryAssign(v)) {                       // lost the race? loop and pick another
                Ticket t = new Ticket("T-" + seq.incrementAndGet(), v, spot, now);
                open.put(t.id(), t);
                return t;
            }
        }
        throw new LotFullException(v.type());
    }

    public long unpark(String ticketId, Instant exitAt) {
        Ticket t = open.remove(ticketId);                  // remove == this ticket can never bill twice
        if (t == null) throw new IllegalArgumentException("Unknown or already-closed ticket: " + ticketId);
        long amount = pricing.fee(t, exitAt);
        t.markPaid();
        t.spot().release();
        return amount;
    }

    public long availableSpots(SpotSize size) {
        return spots.stream().filter(ParkingSpot::isFree).filter(s -> s.size() == size).count();
    }
}

// ---------- the demo that has to run ----------
public class Main {
    public static void main(String[] args) {
        List<ParkingSpot> spots = new ArrayList<>();
        SpotSize[][] layout = {
            { SpotSize.SMALL, SpotSize.SMALL, SpotSize.MEDIUM, SpotSize.MEDIUM, SpotSize.LARGE },
            { SpotSize.SMALL, SpotSize.MEDIUM, SpotSize.MEDIUM, SpotSize.LARGE,  SpotSize.LARGE }
        };
        for (int f = 0; f < layout.length; f++)
            for (int i = 0; i < layout[f].length; i++)
                spots.add(new ParkingSpot("F" + (f + 1) + "-" + (i + 1), f, layout[f][i]));

        ParkingLot lot = new ParkingLot(spots, new NearestFirst(), new HourlyRate());
        Instant nine = Instant.parse("2026-01-01T09:00:00Z");

        Ticket bike  = lot.park(new Bike("KA-01-0001"),  nine);
        Ticket car   = lot.park(new Car("KA-05-1007"),   nine);
        Ticket truck = lot.park(new Truck("KA-09-4242"), nine);
        System.out.println("parked -> " + bike.spot().id() + ", " + car.spot().id() + ", " + truck.spot().id());
        System.out.println("free MEDIUM spots: " + lot.availableSpots(SpotSize.MEDIUM));

        System.out.println("car bill  = " + lot.unpark(car.id(), nine.plus(Duration.ofHours(3))) + " paise");

        lot.setPricing(new SlabRate());                    // <-- the follow-up, one line
        System.out.println("bike bill = " + lot.unpark(bike.id(), nine.plus(Duration.ofHours(3))) + " paise");

        try {
            for (int i = 0; i < 5; i++) lot.park(new Truck("KA-09-90" + i), nine);
        } catch (LotFullException e) {
            System.out.println("rejected  -> " + e.getMessage());
        }
    }
}

References & further reading

8 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 08

A bike arrives. Three SMALL spots, two MEDIUM and one LARGE are free. Which spot should the lot hand out?

question 02 / 08

Why does unpark(ticketId, exitAt) take the exit time as a parameter instead of calling the clock inside?

question 03 / 08

The interviewer says: “Now charge a different rate on weekends.” What should your answer be?

question 04 / 08

Two entry gates process arrivals at the same instant and both read that spot F1-03 is free. What is the correct fix?

question 05 / 08

What does the Ticket need to carry for billing to work at all?

question 06 / 08

You are at minute 45 with park() working, unpark() half-written, and no strategy interfaces yet. What do you do?

question 07 / 08

Why model SpotSize as an ordered enum (SMALL < MEDIUM < LARGE) rather than as free-form strings?

question 08 / 08

Which of these is the clearest sign that a parking-lot solution is over-engineered?

0/8 answered