Based on the requirements and use cases, identify the main objects of the system and analyze how they interact and relate to each other...
Core objects
ParkingAttendant — the entry point / facade. It receives entry and exit requests and delegates everything to the parking lot. It knows nothing about spots, tickets, or fees; it just forwards.
ParkingLot — the orchestrator. It owns the floors and the list of issued tickets, and coordinates the three collaborators it depends on: a ticket creator (IParkingTicket), a fee calculator (ICalculationStrategy), and a payment factory (IPaymentFactory). On entry it finds a spot and issues a ticket; on exit it frees the spot, calculates the fee, and triggers payment.
ParkingFloor — a container of spots. It handles the actual assign/unassign logic (with a lock for thread safety) and initializes its own mix of small/medium/large spots.
ParkingSpot — the physical unit. It knows its size (Spot), whether it's available, and what vehicle occupies it. Its key responsibility is CanFit(vehicle) — the size-compatibility rule.
ParkingTicket — the record. Captures the vehicle, spot type, spot id, entry timestamp, and eventually the total fee. It's the token that links an exit back to a specific spot.
Fee calculation subsystem — CalculationStrategy composes three independent factories, each producing a strategy: time-of-day (day/night), vehicle type (car/bike/truck), and spot type (small/medium/large). The total fee is the sum of all three.
Payment subsystem — PaymentFactory produces an IPaymentStrategy (credit card, cash, or mobile) based on a string, and each strategy just executes the payment.
Enums — Vehicle (Car, Bike, Truck) and Spot (Small, Medium, Large) are the shared value types used throughout.
Key relationships
The containment chain is the backbone: one ParkingLot contains many ParkingFloors, each floor contains many ParkingSpots, and the lot also tracks many ParkingTickets. These are ownership (composition) relationships — floors and spots don't exist independently of the lot.
The delegation relationship: ParkingAttendant holds an IParkingLot and forwards requests to it, keeping the caller decoupled from the concrete lot.
The dependency relationships all point at interfaces rather than concrete classes: ParkingLot depends on IParkingTicket, ICalculationStrategy, and IPaymentFactory; CalculationStrategy depends on the three fee-factory interfaces. This is the loose-coupling / dependency-inversion aspect of the design.
The factory relationships produce strategies on demand: PaymentFactory creates an IPaymentStrategy, and each of the three fee factories creates its corresponding fee strategy. ParkingLotBuilder is a special case — it creates a fully wired ParkingLot, injecting all the collaborators.
The implementation relationships are the many interface-to-class pairs (every I* interface is realized by one or more concrete classes) — this is what makes the whole system swappable and testable.
The two main flows
On entry, the chain runs attendant → lot → floor → spot: the lot walks its floors asking each to assign a fitting spot, and when one succeeds it uses the ticket creator to issue a ParkingTicket that records the spot.
For each class, define the attributes (data) it will hold and the methods (functions) that operate on the attributes. Ensure they align with the object's responsibilities and adhere to the principle of encapsulation. Write your code in the code editor below.
Explain design tradeoffs you considered. Check and explain whether your design adheres to SOLID principles. Explain how your design can handle changes in scale and whether it would be easy to extend with new functionalities. Identify areas for future improvement...
Design tradeoffs
Strategy + Factory for fee calculation vs. a single method. Your earlier version had one CalculateFee with a hardcoded $10/hour. The current design splits fee logic into three independent dimensions — time-of-day, vehicle type, spot type — each behind its own factory and strategy. The tradeoff: you traded a one-line method for roughly a dozen classes and interfaces. What you bought is that each pricing dimension can change independently (add a "weekend" time strategy without touching vehicle pricing), and the dimensions combine additively in CalculationStrategy. The cost is real, though: for a system this size it's arguably over-engineered, and there's a subtle bug hiding in the complexity (more below). A middle ground would have been a single IPricingStrategy with the three rules inlined, split out only when a second pricing scheme actually appeared.
Linear search vs. indexed lookup. AssignParkingSpot scans every spot on a floor until one fits; HandleExistRequest scans every floor's spot list to find the one matching the ticket. This is O(spots) per operation. The tradeoff is simplicity over speed — fine for a demo, but it directly contradicts your non-functional requirement of "efficient data structures for fast lookup." You accepted simple code now at the cost of performance at scale.
Attendant as a thin facade vs. giving it real responsibility. ParkingAttendant currently just forwards to IParkingLot with identical method signatures. The tradeoff: it adds an indirection layer that today does nothing, but it gives you a natural seam to later add attendant-specific concerns (multiple attendants, request queuing, logging) without touching the lot. Right now it reads as ceremony; whether it pays off depends on whether those concerns ever materialize.
Coarse-grained locking vs. fine-grained. Each floor has a static lock object shared across all floor instances. This serializes every assignment across the entire lot — safe, but it throttles concurrency to one assignment at a time system-wide. The tradeoff was correctness-simplicity over throughput, but the static is almost certainly a mistake rather than an intentional tradeoff (see scale section).
SOLID adherence
Single Responsibility — mostly good, with cracks. Most classes have one clear job: ParkingSpot knows fitting, ParkingFloor manages spots, each fee strategy owns one rule. The crack is ParkingLot.HandleExistRequest, which does ticket lookup, spot release, fee calculation, and payment orchestration in one method. That's four responsibilities in one place. ParkingTicket also mixes being a data record with implementing IParkingTicket (a factory that creates other tickets) — the object is both the product and the factory, which muddies its responsibility.
Open/Closed — strong for pricing and payment, weak elsewhere. Adding a new payment type or a new fee-strategy dimension is clean-ish, but the factories violate OCP: PaymentFactory.CreatePayment is an if/else chain on a string, so adding "ApplePay" means editing that method. Same for the three fee factories. True OCP would register strategies in a dictionary or use DI so new types plug in without modifying the factory. And CanFit hardcodes the vehicle-to-spot rules in a conditional — a new vehicle type forces you to edit that method.
Liskov — a genuine violation lurking. Your vehicle-type fee strategies each check the type and return 0 if it doesn't match (CarTypeWiseFeeStrategy returns 20 only for a car, else 0). This means the factory could hand you the wrong strategy and it would silently return 0 rather than fail. The strategies aren't cleanly substitutable — each only works for its one input, so passing the "wrong" vehicle produces a wrong-but-silent answer. A cleaner design has each strategy assume it's only ever called for its type, or returns a rate without re-checking.
Interface Segregation — good. Interfaces are small and focused (IParkingSpot has three methods, IPaymentStrategy has one). Nobody's forced to implement methods they don't use. One nit: IParkingTicket bundles CreateTicket, CalculateDuration, and LookUpParkingTicketByParkingId — those are arguably three different concerns that a client rarely needs all of.
Dependency Inversion — this is the design's real strength. ParkingLot depends on IParkingTicket, ICalculationStrategy, IPaymentFactory — all abstractions, all injected via constructor. CalculationStrategy depends on factory interfaces. This is textbook DIP and it's what makes the system testable. The one leak: ParkingLotBuilder news up all the concrete classes itself, so the composition root is hardcoded rather than fed by a DI container — acceptable, since something has to know the concretes, but it means swapping implementations requires editing the builder.
Scale and extensibility
Where it holds up. Adding a new payment method, a new pricing dimension, or a new floor is genuinely easy — the interfaces absorb those changes. Supporting more floors is just more list entries; the multi-floor requirement is satisfied structurally.
Where it breaks under load. Three things bite as the lot grows:
The static object _lockObject on ParkingFloor is shared by every floor, so all assignments across all floors serialize through one lock. A 20-floor garage gets no more concurrency than a 1-floor one. This should be a per-instance lock (drop static), and ideally per-spot-type so different vehicle classes don't contend.
The linear scans mean lookup cost grows with lot size. At thousands of spots, both entry (find a fit) and exit (find the ticket's spot) get slow. Fixing this means maintaining an availability index — e.g. a Queue
Ticket lookup is a LINQ Where over the full ticket list on every exit — also linear. A Dictionary
Extensibility verdict. New pricing/payment/floors: easy. New vehicle types or spot types: not easy — you'd edit CanFit, add a vehicle fee strategy and factory branch, add nothing for spot rules but touch the factory if/else. The design is extensible along the axes it was built for and rigid along the ones it wasn't.
Areas for future improvement
Ordered roughly by payoff:
Fix the concurrency correctness bug. The static lock is wrong, but there's a deeper issue: in AssignParkingSpot the CanFit check happens outside the lock and Assign happens inside. Two threads can both see the same spot as fittable, then both assign it. The check-and-assign must be atomic — move CanFit inside the lock, or lock per spot.
Wire lookups through dictionaries/queues as above — this is the single biggest step toward the stated performance and scalability NFRs.
Extract exit orchestration. Pull fee-calc + payment out of ParkingLot.HandleExistRequest into something like a CheckoutService, restoring single responsibility and making the exit flow unit-testable in isolation.
Separate the ticket record from ticket creation. Make ParkingTicket a pure data object and move CreateTicket into a dedicated TicketFactory/ITicketService. Right now a ticket instance creating other tickets is conceptually odd.
Replace factory if/else chains with a registry. A Dictionary
Make pricing configurable, not hardcoded. The rates (20, $30…) are baked into strategy classes. Your NFR calls for configurability without code changes — load rates from config so pricing changes don't require a recompile.
Harden input validation and the fee/duration coupling. HandleExistRequest handles a bad Guid gracefully now (good), but note CalculationStrategy recomputes duration internally while ParkingTicket.CalculateDuration also exists and is never called — dead code signaling the duration concern isn't cleanly owned. Pick one home for it.
Add transactional integrity. Your ACID NFR isn't met — if payment fails after the spot is freed, the spot is lost and no money collected. The exit sequence needs a defined ordering (calculate → collect payment → only then release) or compensation logic.
Introduce a real DI container at the composition root so ParkingLotBuilder stops hand-newing dependencies.
The short version: the design's spine — dependency inversion through injected interfaces — is solid and is what makes everything else improvable without a rewrite. The gaps are concentrated in the runtime concerns (concurrency correctness, lookup performance, transactional safety) that the class structure alone doesn't address, plus a couple of factories that promise open/closed but deliver if/else.