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.