Low-Level Design (LLD) — Object Design, UML and Classic Problems in C#
Clarify requirements and scope → list use cases → find entities (nouns) and behaviours (verbs) → define relationships (composition, inheritance, interfaces) → draw a class diagram → apply SOLID and suitable patterns (Strategy, State, Factory, Observer) → write code for the main flows → discuss edge cases, concurrency, extensibility, testing and persistence.
In short: Prefer composition ("has-a") because it keeps classes small and flexible: behaviour can be swapped at runtime via interfaces (Strategy) and changes don't ripple through a hierarchy.
- Use inheritance only for genuine "is-a" relationships with shared behaviour and substitutability (Liskov).
- Deep inheritance trees become rigid and fragile.
In short: Association: objects know/use each other (Teacher ↔ Student).
- Aggregation: whole–part where the part can exist independently (Team has Players; players exist without the team) — hollow diamond.
- Composition: strong ownership where the part's lifetime depends on the whole (House has Rooms; Order has OrderLines) — filled diamond.
In short: ParkingLot (facade/singleton via DI) composed of Floors composed of Spots;
- Vehicle hierarchy (Car, Bike, Truck) or a type enum;
- Ticket;
- FeeCalculator with Strategy per vehicle type;
- optional Observer for display boards;
- Factory for vehicles.
- Concurrency: lock or atomic spot allocation so two gates can't take one spot.
- Extensibility: new vehicle/spot types and pricing rules without modifying existing classes.
In short: When an object's behaviour changes with its internal state and you'd otherwise have large switch statements on state.
- Each state is a class implementing the same interface; the context delegates to the current state, and states trigger transitions.
- Examples: vending machine (Idle, HasMoney, Dispensing, SoldOut), order lifecycle, ATM, elevator, document approval.
In short: Identify shared mutable state and make the critical operation atomic: locks around allocation in a single process, atomic compare-and-set (Interlocked, ConcurrentDictionary.TryAdd), optimistic concurrency (row version) or unique constraints in the database, distributed locks with TTL (Redis SET NX PX) across servers.
- Seat booking: temporary locks with expiry, confirm on payment, unique (ShowId, SeatId) constraint as the final guarantee.
In short: Class diagrams (classes, attributes, methods, interfaces, relationships, multiplicities) and sequence diagrams (order of calls between objects for a use case).
- Sometimes state diagrams (lifecycle of an order/elevator) and activity diagrams.
- Keep them simple and focused on the core flow.
Program to interfaces, isolate what varies (Strategy for rules, Factory for creation, Observer for notifications, Decorator for add-ons), keep classes small with single responsibilities, use dependency injection, avoid type switches spread across the code (Open/Closed), and keep domain logic independent of UI/database so it can be tested and reused.
In short: A hash map from key to a node in a doubly linked list ordered by recency.
- Get: find the node via the map, move it to the front, return the value.
- Put: if present, update and move to front; otherwise insert at the front, and if capacity is exceeded remove the tail node and its map entry.
- Add a lock for thread safety.
- In C#:
Dictionary<K, LinkedListNode<(K,V)>>+LinkedList<(K,V)>.
In short: Classes: ElevatorSystem/Controller, Elevator (current floor, direction, stop queues), Request (floor, direction), Button/Panel, Dispatcher with a strategy (nearest car, least load, zone-based).
- Each elevator serves stops in its current direction (SCAN) before reversing.
- Use the State pattern for Moving/Stopped/Maintenance, Command for requests, and simulate movement with ticks; handle emergency stop and overweight.
In short: Chain of Responsibility: handlers for ₹2000, ₹500, ₹200 and ₹100 notes;
- each dispenses as many notes as it can (
amount / denomination, limited by its stock) and passes the remainder to the next handler; - if a remainder is left at the end, the request fails and nothing is dispensed (check first, then commit).
- Around it: ATM with the State pattern (Idle → CardInserted → Authenticated → Transaction), a BankService interface for balance/debit, and transaction logging.
In short: An INotificationChannel interface (SendAsync(recipient, message)) with one class per channel (Strategy);
- a factory or DI registration resolves channels by user preferences;
- a
NotificationServicebuilds messages from templates and sends to each selected channel; - Observer/event hooks trigger notifications from domain events (OrderShipped).
- New channels (WhatsApp) = a new class + registration (Open/Closed).
- Add retries and a queue for reliability.
In short: Each entity with identity becomes a table with a primary key;
- value objects become columns (or owned types);
- one-to-many → foreign key on the "many" side;
- many-to-many → junction table;
- inheritance → table-per-hierarchy with a discriminator column (simple, fast) or table-per-type (normalised);
- enums stored as int/string;
- add unique constraints for business rules (one booking per seat per show) and indexes for the main queries.
Correctly modelled entities and relationships, small cohesive classes with clear names, dependencies on interfaces for varying behaviour, appropriate (not excessive) patterns, valid states enforced by constructors/methods, handled edge cases and concurrency, testable code (inject time, randomness, external services), and the ability to explain trade-offs and how new requirements fit without rewriting.
In short: 10 min: requirements and class diagram (User, Group, Expense, Split strategies, Ledger/BalanceSheet).
- 50 min: implement core — add expense with equal/exact/percent split (Strategy), update pairwise balances, show balances per user, validation (percentages sum to 100, exact amounts sum to total).
- 20 min: simplify debts (greedy matching of max creditor and max debtor), simple console/driver and a few unit tests.
- 10 min: refactor names, handle rounding, explain extensions (currencies, persistence, notifications).
if (vehicle is Car) ... else if (vehicle is Bike) ... in five places. The interviewer adds "electric cars with charging spots". What do you change?In short: The type checks violate Open/Closed — every new type edits five places.
- Move varying behaviour behind abstractions: a spot-type mapping/strategy per vehicle type, fee strategies registered in a dictionary/DI, and a
ChargingSpottype with its own pricing (Decorator/Strategy for charging cost). - Adding electric cars then means adding new classes and registering them, not modifying existing logic.