Part 2: turning the class design into a working allocation strategy, a swappable fee strategy, and handling the concurrency question every interviewer eventually asks.
Published September 23, 2026
Building on Parking Lot — Requirements & Class Design, this part makes two specific pieces of behavior swappable and handles the concurrency question that naturally follows.
Part 1's findAvailableSpot just took the first fit. A real system might want "nearest to the entrance" or "match vehicle size as tightly as possible" (don't waste a large spot on a motorcycle) — exactly the shape Strategy Pattern solves.
interface SpotAllocationStrategy {
Optional<ParkingSpot> findSpot(List<ParkingSpot> spots, Vehicle vehicle);
}
class NearestAvailableStrategy implements SpotAllocationStrategy {
public Optional<ParkingSpot> findSpot(List<ParkingSpot> spots, Vehicle vehicle) {
return spots.stream().filter(s -> !s.isOccupied() && s.canFit(vehicle)).findFirst(); // spots pre-sorted by distance
}
}
class BestFitStrategy implements SpotAllocationStrategy {
public Optional<ParkingSpot> findSpot(List<ParkingSpot> spots, Vehicle vehicle) {
return spots.stream()
.filter(s -> !s.isOccupied() && s.canFit(vehicle))
.min(Comparator.comparing(ParkingSpot::sizeRank)); // smallest spot that still fits — avoids wasting large spots
}
}
ParkingLot now takes a SpotAllocationStrategy in its constructor instead of hard-coding "first fit" — the allocation policy is chosen once, at construction, and can be swapped without touching ParkingLot's own code at all, exactly matching the Strategy Pattern's core benefit.
interface FeeStrategy {
double calculateFee(Ticket ticket, Instant exitTime);
}
class HourlyFeeStrategy implements FeeStrategy {
private static final double RATE_PER_HOUR = 5.0;
public double calculateFee(Ticket ticket, Instant exitTime) {
long minutes = Duration.between(ticket.getEntryTime(), exitTime).toMinutes();
return Math.ceil(minutes / 60.0) * RATE_PER_HOUR; // round up to the next full hour
}
}
class FlatDailyRateStrategy implements FeeStrategy {
public double calculateFee(Ticket ticket, Instant exitTime) {
return 25.0; // flat rate regardless of duration, e.g. for an event lot
}
}
Separating FeeStrategy from SpotAllocationStrategy (rather than one big "ParkingPolicy" class doing both) follows the Single Responsibility Principle directly — a change to pricing shouldn't risk touching allocation logic, and vice versa, and each can be tested and swapped completely independently.
The naive findSpot() + park() sequence from Part 1 has an obvious race: two threads could both see the same spot as available before either marks it occupied — the classic check-then-act race (see Concurrent Utilities & Coordination and HashMap Concurrency Variants for the same shape of bug elsewhere).
class ParkingSpot {
private final ReentrantLock lock = new ReentrantLock(); // per-spot lock — fine-grained, not a lock on the whole lot
private volatile boolean occupied = false;
private Vehicle parkedVehicle;
boolean tryPark(Vehicle vehicle) {
lock.lock();
try {
if (occupied) return false; // lost the race — someone else got here first
occupied = true;
parkedVehicle = vehicle;
return true;
} finally {
lock.unlock();
}
}
}
Where to put the lock matters: a single lock guarding the entire ParkingLot would be correct but serializes every parking attempt across the whole facility — a huge, unnecessary bottleneck for a large lot with hundreds of independent spots. Locking per spot (as above) means only two threads racing for the same specific spot ever contend at all; threads targeting different spots proceed fully in parallel. This is the same fine-grained-locking principle behind ConcurrentHashMap's per-bucket locking (see HashMap Concurrency Variants) — lock the smallest unit that actually needs protecting, not the whole structure.
The calling code now needs to handle a failed tryPark() by retrying against the next candidate spot, not just giving up — the allocation strategy's findSpot() returns a candidate, but tryPark() is the actual atomic claim, and losing that race means falling back to the next candidate in the list.
Q: Why volatile AND a lock on the same field — isn't that redundant?
A: The lock protects the compound check-then-set operation (atomicity); volatile on occupied additionally guarantees that a thread reading the spot's status without acquiring the lock (e.g. for a dashboard showing live occupancy) still sees the latest value rather than a stale cached one — belt-and-suspenders for two different guarantees, not true redundancy.
Q: Could you avoid locking entirely using CAS instead?
A: Yes — an AtomicBoolean occupied with compareAndSet(false, true) achieves the same atomic claim without an explicit lock, and for a simple boolean flip like this, CAS is arguably a cleaner fit than a full ReentrantLock (see Visibility & Memory Model's coverage of CAS-based atomic classes).
Q: How would FeeStrategy and SpotAllocationStrategy be chosen at runtime — hard-coded, or configurable?
A: A Factory (see Factory & Abstract Factory) is the natural fit — a ParkingLotFactory could construct a ParkingLot with different strategy combinations for different facility types (an airport lot with BestFitStrategy + HourlyFeeStrategy, an event lot with NearestAvailableStrategy + FlatDailyRateStrategy), keeping that combinatorial choice in one place.