Roadmap › Low-Level Design (LLD) — Object Design, UML and Classic Problems in C# › Module: Classic LLD Problems
Lessons in this course (8)
4. Design a parking lot
Intermediate★ Must knowLesson 4 of 8
Requirements: multiple floors; spot types (Bike, Compact, Large); vehicles (Bike, Car, Truck); entry issues a ticket; exit calculates the fee by type and duration; show free spots; thread-safe allocation.
csharp
public enum VehicleType { Bike, Car, Truck }
public enum SpotType { Bike, Compact, Large }
public abstract record Vehicle(string Number, VehicleType Type);
public sealed record Car(string Number) : Vehicle(Number, VehicleType.Car);
public sealed record Bike(string Number) : Vehicle(Number, VehicleType.Bike);
public sealed record Truck(string Number) : Vehicle(Number, VehicleType.Truck);
public sealed class Spot(string id, SpotType type)
{
public string Id { get; } = id;
public SpotType Type { get; } = type;
public Vehicle? Vehicle { get; private set; }
public bool IsFree => Vehicle is null;
public void Occupy(Vehicle v) => Vehicle = IsFree ? v : throw new InvalidOperationException($"Spot {Id} is taken");
public void Release() => Vehicle = null;
}
public sealed class Floor(int number, IEnumerable<Spot> spots)
{
public int Number { get; } = number;
private readonly List<Spot> _spots = spots.ToList();
public Spot? FindFree(SpotType type) => _spots.FirstOrDefault(s => s.IsFree && s.Type == type);
public int FreeCount(SpotType type) => _spots.Count(s => s.IsFree && s.Type == type);
}
public sealed record Ticket(Guid Id, Vehicle Vehicle, Spot Spot, DateTime EntryUtc);
public sealed class ParkingLot(IEnumerable<Floor> floors, FeeCalculator fees, TimeProvider clock)
{
private readonly List<Floor> _floors = floors.ToList();
private readonly Dictionary<Guid, Ticket> _active = [];
private readonly Lock _gate = new(); // C# 13 System.Threading.Lock
private static SpotType SpotFor(VehicleType t) => t switch
{
VehicleType.Bike => SpotType.Bike,
VehicleType.Car => SpotType.Compact,
_ => SpotType.Large
};
public Ticket Park(Vehicle v)
{
lock (_gate) // two gates can't grab the same spot
{
var spot = _floors.Select(f => f.FindFree(SpotFor(v.Type))).FirstOrDefault(s => s != null)
?? throw new InvalidOperationException("Parking full for " + v.Type);
spot.Occupy(v);
var ticket = new Ticket(Guid.NewGuid(), v, spot, clock.GetUtcNow().UtcDateTime);
_active[ticket.Id] = ticket;
return ticket;
}
}
public decimal Unpark(Guid ticketId)
{
lock (_gate)
{
if (!_active.Remove(ticketId, out var t)) throw new KeyNotFoundException("Unknown ticket");
t.Spot.Release();
return fees.For(t.Vehicle.Type, clock.GetUtcNow().UtcDateTime - t.EntryUtc);
}
}
}Discussion points: Strategy for fees (new pricing without changing ParkingLot), TimeProvider for testable time, lock (or per-spot compare-and-swap) for concurrency, Observer to update display boards, persistence tables Spots, Tickets, Payments.