Entity Discovery and Demotion
TL;DR: Extract nouns from your requirements to get entity candidates. Then aggressively filter: if it doesn't maintain changing state or enforce rules, it's a field on another class, not a class of its own. Most nouns get demoted.

From Requirements to Entities
Once you have a scoped requirements list, the next step is identifying the classes you'll actually build. The technique is simple: pull out every noun from your requirements, then decide which ones deserve to be classes.
Start with a Parking Lot example. Suppose your requirements are:
1. Vehicles enter the lot and receive a ticket
2. The lot has spots of different sizes (small, medium, large)
3. Vehicles are assigned to spots matching their size
4. On exit, fee is calculated based on duration
5. The lot tracks available spots per size
Noun extraction gives you these candidates:
Seven candidates. In the final design, you'll likely have 3-4 classes. The rest get demoted.
The Critical Filter
For each candidate noun, ask one question:
"Does it maintain changing state or enforce rules? If not, it's a field, not a class."
This single question eliminates most of your candidates. A class earns its existence by managing complexity — tracking state that changes over time, enforcing constraints, or coordinating behavior. If a noun is just a label, a measurement, or a static attribute, it belongs as a field on a class that does manage complexity.
Let's apply the filter to our candidates:
| Candidate | Maintains state? Enforces rules? | Verdict |
|---|---|---|
| Vehicle | No. It has a type and a license plate, but those don't change. | Demote to enum VehicleType |
| Lot | Yes. Tracks available spots, assigns vehicles, manages entry/exit flow. | Keep as ParkingLot class |
| Ticket | Yes. Links a vehicle to a spot, records entry time, used for fee calculation. | Keep as Ticket class |
| Spot | Maybe. If spots have individual state (occupied/available), it could be a class. But often a spot is just an ID + size. | Demote to a size attribute — the lot tracks which spots are occupied |
| Size | No. It's a fixed category. | Demote to enum SpotSize |
| Fee | No. It's a calculated value, not a stateful entity. | Demote to a return value from calculateFee() |
| Duration | No. It's derived from entry time and exit time. | Demote to a calculation inside calculateFee() |
Result: 2 classes (ParkingLot, Ticket) plus 2 enums (VehicleType, SpotSize). Clean and focused.
Entity Demotion Examples
This pattern repeats across every LLD problem. Here are common demotions that trip up candidates:
Package in Amazon Locker
Candidates often create a Package class with fields like id, size, recipientEmail. But a package doesn't do anything in the locker system — it has no behavior, no changing state. The locker system only cares about the package's size (to find a fitting compartment) and its pickup code.
// BAD: Package as a class that does nothing
class Package {
private String id;
private Size size;
private String recipientEmail;
// ...getters, no real behavior
}
// GOOD: Package is just a size parameter
class LockerSystem {
public String storePackage(Size packageSize) {
Compartment compartment = findAvailable(packageSize);
compartment.occupy();
return compartment.generatePickupCode();
}
}
// BAD
class Package {
std::string id;
Size size;
std::string recipientEmail;
};
// GOOD
class LockerSystem {
public:
std::string storePackage(Size packageSize) {
Compartment& compartment = findAvailable(packageSize);
compartment.occupy();
return compartment.generatePickupCode();
}
};
# BAD
class Package:
def __init__(self, id: str, size: Size, recipient_email: str):
self._id = id
self._size = size
self._recipient_email = recipient_email
# GOOD
class LockerSystem:
def store_package(self, package_size: Size) -> str:
compartment = self._find_available(package_size)
compartment.occupy()
return compartment.generate_pickup_code()
Vehicle in Parking Lot
A vehicle has a type and a license plate. Neither changes while it's in the lot. The lot doesn't need to call methods on the vehicle — it just needs to know its type to assign the right spot size.
// BAD: Vehicle class with no behavior
class Vehicle {
private VehicleType type;
private String licensePlate;
// ...just getters
}
// GOOD: Vehicle is an enum
enum VehicleType { SMALL, MEDIUM, LARGE }
class ParkingLot {
public Ticket enter(VehicleType vehicleType, String licensePlate) {
int spotId = assignSpot(vehicleType);
return new Ticket(spotId, licensePlate, Instant.now());
}
}
// GOOD
enum class VehicleType { SMALL, MEDIUM, LARGE };
class ParkingLot {
public:
Ticket enter(VehicleType vehicleType, const std::string& licensePlate) {
int spotId = assignSpot(vehicleType);
return Ticket(spotId, licensePlate, Clock::now());
}
};
# GOOD
class VehicleType(Enum):
SMALL = "small"
MEDIUM = "medium"
LARGE = "large"
class ParkingLot:
def enter(self, vehicle_type: VehicleType, license_plate: str) -> Ticket:
spot_id = self._assign_spot(vehicle_type)
return Ticket(spot_id, license_plate, datetime.now())
Seat in Movie Booking
A seat in a cinema doesn't change. Row B, Seat 7 is always Row B, Seat 7. The interesting state is whether a seat is reserved, and that's managed by the Reservation class or the Showtime class — not by the seat itself.
// BAD: Seat as a class
class Seat {
private String row;
private int number;
private boolean isBooked; // This state belongs to the booking system
}
// GOOD: Seat is a string identifier
class Showtime {
private Map<String, Reservation> reservedSeats; // "B7" -> Reservation
public boolean isSeatAvailable(String seatId) {
return !reservedSeats.containsKey(seatId);
}
}
// GOOD
class Showtime {
std::unordered_map<std::string, Reservation> reservedSeats;
public:
bool isSeatAvailable(const std::string& seatId) const {
return reservedSeats.find(seatId) == reservedSeats.end();
}
};
# GOOD
class Showtime:
def __init__(self):
self._reserved_seats: dict[str, Reservation] = {}
def is_seat_available(self, seat_id: str) -> bool:
return seat_id not in self._reserved_seats
When Something IS a Class
Not everything gets demoted. Here are signs that a noun deserves to be a full class:
It has lifecycle state. An AccessToken starts valid, can be refreshed, and eventually expires. That's changing state with rules around transitions.
class AccessToken {
private String token;
private Instant expiresAt;
private boolean revoked;
public boolean isValid() {
return !revoked && Instant.now().isBefore(expiresAt);
}
public void revoke() {
this.revoked = true;
}
}
class AccessToken {
std::string token;
TimePoint expiresAt;
bool revoked = false;
public:
bool isValid() const {
return !revoked && Clock::now() < expiresAt;
}
void revoke() { revoked = true; }
};
class AccessToken:
def __init__(self, token: str, expires_at: datetime):
self._token = token
self._expires_at = expires_at
self._revoked = False
def is_valid(self) -> bool:
return not self._revoked and datetime.now() < self._expires_at
def revoke(self) -> None:
self._revoked = True
It enforces constraints. A Reservation links a user to specific seats for a specific showtime, and enforces that you can't double-book. That's a rule, not just data.
It coordinates multiple pieces of data. A Ticket ties together a spot ID, a license plate, and an entry time — and those pieces are meaningless without each other.
Interview tip: When presenting your entities to the interviewer, briefly explain what you demoted and why. "I considered making Vehicle a class, but it has no behavior or changing state in our system — VehicleType as an enum is sufficient." This demonstrates the exact filtering skill they're evaluating.
Full Walkthrough: Parking Lot Entity Discovery
Let's do the complete noun extraction and filtering process step by step, showing every decision.
Requirements:
1. Multi-story parking lot with spots of different sizes
2. Vehicles enter and are assigned to a matching spot on any floor
3. Vehicles receive a ticket on entry with entry time and spot info
4. On exit, fee is calculated based on hourly rate and duration
5. System tracks available spots per floor and per size
6. Different fee rates for different vehicle types
Step 1: Raw noun extraction
Read every requirement and pull out every noun, no filtering yet:
14 nouns. Now filter.
Step 2: Immediate demotions (obvious non-classes)
| Noun | Why it's not a class | Demoted to |
|---|---|---|
| Size | Fixed category (SMALL, MEDIUM, LARGE) | Enum SpotSize |
| Type | Fixed category (MOTORCYCLE, CAR, TRUCK) | Enum VehicleType |
| Entry | Not a thing -- it's an event | Parameter to a method |
| Time | Primitive value | Instant / datetime field |
| Duration | Derived from entry time and exit time | Calculated value |
| Info | Vague -- "spot info" means spotId + floor | Fields on Ticket |
| Rate | A number (dollars per hour) | Field or config value |
| System | Too abstract -- what IS the system? | This becomes the orchestrator |
Step 3: Evaluate remaining candidates
| Candidate | Does it maintain changing state? | Does it enforce rules? | Verdict |
|---|---|---|---|
| Lot | Yes -- tracks occupied spots, manages entry/exit flow | Yes -- enforces capacity limits, spot assignment | Class: ParkingLot (orchestrator) |
| Floor | Maybe -- could track spots per floor | Maybe -- could manage floor-level capacity | Evaluate further |
| Spot | Minimal -- a spot has an ID, size, and floor. These don't change. | No rules. | Demote to fields (spotId + spotSize + floor) |
| Vehicle | No -- type and plate don't change while parked | No rules | Demote to enum VehicleType |
| Ticket | Yes -- created on entry, used on exit, links vehicle to spot + time | Enforces "one ticket per vehicle" | Class: Ticket |
| Fee | No -- it's a calculated result | Enforces rate rules | Evaluate further |
Step 4: Resolve "evaluate further" candidates
Floor: Does floor need to be a class? It depends on the design. If each floor independently tracks its own spots, making Floor a class that owns Set<Integer> occupiedSpots is reasonable. But you can also have the ParkingLot track occupiedSpotIds across all floors with each spot knowing its floor. The simpler design has fewer classes.
Decision: Demote Floor. The ParkingLot tracks spots globally. Floor is a field on each spot's metadata.
Fee: Fee is a calculated value, not a stateful entity. But fee calculation has rules (different rates per vehicle type, hourly calculation). Does the calculation logic deserve its own class?
Decision: Extract FeeCalculator as a class. It encapsulates the pricing rules and can be swapped out (Strategy pattern) for different pricing models.
Step 5: Final entity list
Five types total. Clean, focused, each with a clear purpose.
Full Walkthrough: Movie Booking Entity Discovery
Requirements:
1. Theater has multiple screens with fixed seat layouts
2. Movies have showtimes assigned to screens
3. Users browse showtimes and select available seats
4. Users book seats -- if two users pick the same seat, first to confirm wins
5. Users can cancel bookings
Noun extraction:
Filtering:
| Candidate | Maintains state? | Enforces rules? | Verdict |
|---|---|---|---|
| Theater | Yes -- owns screens | Coordinates bookings | Class (orchestrator, or could be BookingSystem) |
| Screen | Minimal -- has an ID and seat count, doesn't change | No | Demote to screenId field |
| Seat | No -- Row B Seat 7 never changes | No | Demote to string identifier "B7" |
| Layout | No -- fixed grid dimensions | No | Demote to rows/cols fields on screen metadata |
| Movie | No -- title and duration don't change during booking | No | Demote to movieId/title field |
| Showtime | Yes -- tracks which seats are reserved for this showing | Yes -- prevents double booking | Class |
| User | No meaningful state for the booking system | No | Demote to userId string |
| Booking | Yes -- links user to seats for a showtime, can be cancelled | Yes -- enforces cancellation rules | Class |
| Confirmation | No -- it's an event, not a thing | No | Demote to return value |
Final entities:
Classes: BookingSystem (orchestrator), Showtime, Booking
Enums: SeatStatus (AVAILABLE, RESERVED, if needed)
Three classes. Movie, Screen, Seat, User -- all demoted. This surprises candidates who expect a Seat class, but a seat is just a position identifier. The interesting state is whether a seat is reserved for a specific showtime, which is managed by Showtime or Booking.
Full Walkthrough: Splitwise Entity Discovery
Requirements:
1. Users add expenses with a payer and list of participants
2. Expenses split equally among participants
3. System tracks who owes whom (net balances)
4. Users can settle debts
5. System shows each user's total balance
Noun extraction:
Filtering:
| Candidate | Maintains state? | Enforces rules? | Verdict |
|---|---|---|---|
| User | Maybe -- has a name and running balance | Minimal rules | Evaluate |
| Expense | Yes -- records who paid, who participated, the amount | Yes -- must have at least 2 participants, positive amount | Class |
| Payer | No -- it's a role, not a thing | No | Demote to userId field on Expense |
| Participant | No -- it's a role | No | Demote to List |
| Split | No -- it's a calculated value (amount / participants) | No | Demote to calculation in Expense |
| Balance | No -- it's a derived value | No | Demote to calculated property |
| Debt | No -- it's a relationship (A owes B $X) | No | Demote to entry in a Map |
| Settlement | Maybe -- records a payment between users | Enforces amount rules | Evaluate |
Resolving "evaluate" candidates:
User: In Splitwise, users don't have complex behavior. A userId string is sufficient to identify who paid and who owes. But if you want to track "all expenses a user is part of" or "user's total balance," you could have a User class. The simpler path: keep user as a string ID and compute balances from the expense history.
Decision: Demote User to string ID.
Settlement: A settlement is effectively an expense where one person "pays back" another. You could model it as a special type of Expense, or as its own class. Since settlement has different semantics (it reduces debt, not creates it), a separate class is cleaner.
Decision: Keep Settlement as a class, or model it as a method settle(fromUser, toUser, amount) on the orchestrator.
Final entities:
Classes: ExpenseManager (orchestrator), Expense
Enums: SplitType (EQUAL -- extensible to PERCENTAGE, EXACT later)
Two core classes. The orchestrator manages the list of expenses and computes balances. Each Expense records who paid, who participated, and how much. Settlements are handled as a method on ExpenseManager that adjusts the balance ledger.
Over-Modeling vs Under-Modeling: What Goes Wrong
Over-modeling: Too many classes
Parking Lot with over-modeling:
Vehicle, ParkingLot, Spot, Floor, Gate, EntryGate, ExitGate,
Ticket, Receipt, Fee, FeeCalculator, FeeRule, Duration,
LicensePlate, SpotSize, VehicleType, Address, Attendant,
Display, ParkingRecord, PaymentMethod
20 classes. What goes wrong:
- You run out of time. You spend 25 minutes defining class relationships and never implement a single method.
- Your design is hard to explain. The interviewer asks "walk me through parking a car" and you trace through 8 classes.
- Classes have no behavior. Most of them are data holders with getters. The interviewer sees you can define attributes but questions whether you understand behavior-driven design.
- Coupling explodes. Every class depends on 3-4 others. Changing anything touches everything.
Under-modeling: Too few classes
One class. What goes wrong:
- God class. ParkingLot handles entry, exit, spot assignment, fee calculation, ticket management, and display. It's 300 lines with 15 methods.
- No extensibility. "What if we add different fee structures?" requires modifying ParkingLot, which touches everything.
- Can't discuss design. The interviewer asks about class responsibilities and there's nothing to discuss -- everything is in one place.
- Testing is painful. You can't unit-test fee calculation without setting up the entire lot.
The sweet spot
Parking Lot with balanced modeling:
ParkingLot (orchestrator), Ticket, FeeCalculator
+ enums: VehicleType, SpotSize
3 classes + 2 enums. Each class has clear state, clear behavior, and a single reason to change. You can explain the design in 2 minutes, implement the core in 15, and discuss extensions confidently.
More Demotion Examples
Rating in a Ride-Sharing System
Candidates often create a Rating class:
// Over-modeled
class Rating {
private int stars;
private String comment;
private String riderId;
private String driverId;
// ...just getters
}
But a rating doesn't maintain changing state or enforce rules. It's data recorded once and never modified. Demote it to fields on the Trip class:
class Trip {
private int driverRating; // 1-5, set once after trip
private String riderComment; // set once after trip
public void rateDriver(int stars, String comment) {
if (stars < 1 || stars > 5) throw new IllegalArgumentException("Stars must be 1-5");
if (this.driverRating != 0) throw new IllegalStateException("Already rated");
this.driverRating = stars;
this.riderComment = comment;
}
}
The validation (1-5 stars, rate only once) lives on Trip, which already manages the trip lifecycle.
Address in Any System
Address is almost never a class in LLD interviews. It's a string or a set of fields on another entity. The exceptions are rare -- maybe a geocoding service where addresses have behavior (validation, normalization). In a Parking Lot, Library, or E-commerce design, address is just a string.
Notification in Any System
Candidates love creating a Notification class, but in most LLD problems notifications are out of scope. If they're in scope, they're usually just a method call: notificationService.send(userId, message). The notification itself is a string, not a stateful entity.
The Common Mistake: Every Noun Is a Class
Candidates who create a class for every noun end up with designs like this:
Vehicle, ParkingLot, Spot, Floor, Gate, Ticket,
Fee, Duration, LicensePlate, SpotSize, Address,
EntryPoint, ExitPoint, ParkingAttendant...
Fourteen classes. Most of them are data holders with getters and no behavior. The candidate spends 30 minutes wiring together empty shells and never gets to the interesting logic — how spots are assigned, how fees are calculated, how the lot tracks capacity.
Three well-designed classes with real behavior beat fourteen empty ones every time.
Quick Recap
| Concept | What it means | Why it matters |
|---|---|---|
| Noun extraction | Pull every noun from requirements as a candidate entity | Gives you a starting list to filter, not a final class list |
| The critical filter | "Does it maintain changing state or enforce rules?" | Separates real classes from fields/enums/parameters |
| Entity demotion | Downgrading a noun to an enum, a field, or a method parameter | Keeps your design focused on classes that carry real behavior |
| Signs of a real class | Lifecycle state, enforced constraints, coordinated data | These are the entities worth building |