Opening low-level design
Progress is saved as you study.
Opening low-level design
Progress is saved as you study.
Opening your learning path
Progress is saved as you study.
Entity Modeling & Schema Design is a core lesson in the OOP Foundations & Class Design family of the Thita low level design course.
OOP pillars, SOLID principles, entity modeling and class extraction.
It is part of the Interview Soon path: the LLD material that appears in a standard interview loop rather than only in a specialist one.
Entity modeling appears early in most LLD interviews. Start here before services, APIs, or design patterns. First decide what the system stores: classes, relationships, identities, and constraints.
Entity extraction means turning requirement nouns into classes. Every real-world thing the system tracks becomes an entity.
If the requirement says, “A parking lot has multiple floors, each floor has parking spots, vehicles enter, receive tickets, and leave after payment,” the first pass is:
ParkingLotParkingFloorParkingSpotVehicleTicketPaymentAvoid starting with ParkingManager, TicketProcessor, or VehicleService. Those classes may appear after the data model is clear. First identify what exists, what has identity, and how those objects connect.
Entity modeling usually appears in domain-heavy problems:
The interviewer is checking whether you can turn vague requirements into stable class boundaries.
Avoid one giant class that owns everything.
1Bad:
2ParkingLot
3- floors
4- spots
5- vehicles
6- tickets
7- payments
8- calculateFee()
9- assignSpot()
10- openGate()
11- closeGate()ParkingLot now mixes entities, workflows, pricing, and hardware actions. Any change to pricing, gates, or ticketing can break unrelated code.
Better:
1ParkingLot owns ParkingFloors
2ParkingFloor owns ParkingSpots
3Ticket links Vehicle and ParkingSpot
4PricingService calculates fee
5GateService handles entry and exitEntity modeling separates stored domain facts from actions performed on those facts.
A relationship tells you how many objects can be connected. The answer determines where references, collections, and database tables belong.
| Relationship | Meaning | Example | Modeling Choice |
|---|---|---|---|
| 1:1 | One object maps to exactly one other object | User Profile | Store direct reference or foreign key |
| 1:N | One object owns or connects to many objects | User → Orders | Parent has collection, child has parent ID |
| N:M | Many objects connect to many others | Student Course | Use a join entity like Enrollment |
A user has one profile. One profile belongs to one user.
Use a separate Profile when profile data has its own lifecycle, privacy rules, or access pattern. If profile fields are always loaded with the user and managed with the user, keep the fields inside User.
A user can place many orders. Each order belongs to one user.
A common mistake is storing only List<Order> and forgetting that each Order needs its own identity. In databases and distributed systems, Order should store userId instead of a full mutable User object.
A student can enroll in many courses. A course can have many students. Model the relationship as its own object.
Use a join entity when the relationship has its own data. Enrollment can store grade, enrollment date, status, or payment information.
The difference is lifecycle.
Composition means the part dies with the whole. Aggregation means the part can survive independently.
| Type | Lifecycle Rule | Example | Design Implication |
|---|---|---|---|
| Composition | Child cannot exist meaningfully without parent | Order → OrderItems | Parent creates and deletes children |
| Aggregation | Child can exist without parent | Team → Players | Parent references existing objects |
| Plain Association | Objects know each other temporarily | Ticket → Vehicle | Reference does not imply ownership |
An OrderItem has no useful meaning without its Order. If the order is deleted, its items should be deleted too.
Use composition when the parent controls the child’s lifecycle.
A Player can exist without a Team. If a team is deleted, the player remains.
Use aggregation when one object groups another object and does not own its existence.
1Does the child make sense without the parent?
2├── No → Composition
3│ Example: OrderItem without Order
4└── Yes → Aggregation or Association
5 Example: Player without TeamIf lifecycle is unclear in an interview, state your assumption. Example: “I will model Order and OrderItem as composition because deleting an order should delete its items.”
An enum represents a closed set of valid values. Use enums for statuses, types, and modes that reject arbitrary text.
Good examples:
OrderStatus: CREATED, PAID, SHIPPED, CANCELLEDVehicleType: BIKE, CAR, TRUCKPaymentMethod: CASH, CARD, UPI1if status == "paid":
2 shipOrder()
3elif status == "cancelled":
4 refund()Raw strings fail silently when someone writes "PAID", "Paid", "payed", or "cancel".
1if status == OrderStatus.PAID:
2 shipOrder()
3elif status == OrderStatus.CANCELLED:
4 refund()Enums make invalid states harder to represent. Class design should move mistakes from runtime to compile-time or construction-time checks.
Use a database table when values are user-defined or added often without code deployment. Product categories in an e-commerce system usually belong in a table.
Pick enums for fixed business vocabulary. Pick database-backed entities for configurable business vocabulary.
An entity needs an identity that stays stable when attributes change. A user may change email, name, or address. The userId should stay the same.
| ID Strategy | Use When | Example | Recommendation |
|---|---|---|---|
| Auto-increment | Single database, simple CRUD app | order_id = 101 | Use for simple monolith interviews |
| UUID | Distributed creation across services | 550e8400-e29b... | Use when multiple nodes create records |
| Composite key | Identity is a relationship | (studentId, courseId) | Use for join tables |
Use auto-increment when one database owns record creation. It is simple, readable, and enough for most interview designs unless distributed ID generation is part of the problem.
Use UUID when records may be created on different machines before reaching the database. UUIDs avoid coordination and cost readability.
Use composite keys for relationship entities like Enrollment(studentId, courseId). If a student can enroll in the same course only once, this pair is the natural identity.
Default interview choice:
An entity has identity. A value object has no identity and is compared by its values.
| Concept | Compared By | Example | Mutable? |
|---|---|---|---|
| Entity | ID | User, Order, Ticket | Carefully mutable |
| Value Object | Field values | Money, Address, DateRange | Prefer immutable |
| Join Entity | Composite identity | Enrollment | Usually immutable key, mutable metadata |
A User remains the same user after an email change. User is an entity.
An Address with the same street, city, and pin code equals another address with the same values. Address is a value object.
If an object needs history, lifecycle, or references from other objects, model it as an entity. If an object only describes a value and replacement is safer than mutation, model it as a value object.
For a Parking Lot, extract the entities first:
ParkingLot: top-level entity containing floorsParkingFloor: contains parking spotsParkingSpot: has type and occupancyVehicle: has vehicle number and vehicle typeTicket: links vehicle, spot, and entry timeParkingLot → ParkingFloor → ParkingSpot is composition because floors and spots are part of the parking lot model. Ticket → Vehicle is association because the vehicle remains after the ticket is closed.
Extract entities from the nouns, link them with the right relationship, then keep pricing and gate actions in separate services.
1import java.time.Instant;
2import java.util.*;
3
4public class Main {
5 interface Identifiable {
6 String getId();
7 }
8
9 enum VehicleType { BIKE, CAR, TRUCK }
10 enum SpotType { BIKE, COMPACT, LARGE }
11 enum TicketStatus { ACTIVE, PAID }
12
13 static final class Vehicle {
14 private final String number;
15 private final VehicleType type;
16 Vehicle(String number, VehicleType type) {
17 this.number = number; this.type = type;
18 }
19 public String getNumber() { return number; }
20 }
21
22 static final class ParkingSpot implements Identifiable {
23 private final String id;
24 private final SpotType type;
25 private boolean occupied;
26 ParkingSpot(String id, SpotType type) { this.id = id; this.type = type; }
27 @Override public String getId() { return id; }
28 public void occupy() { if (occupied) throw new IllegalStateException("occupied"); occupied = true; }
29 }
30
31 static final class Ticket implements Identifiable {
32 private final String id;
33 private final Vehicle vehicle;
34 private final ParkingSpot spot;
35 private final Instant entryTime;
36 private TicketStatus status = TicketStatus.ACTIVE;
37 Ticket(String id, Vehicle vehicle, ParkingSpot spot) {
38 this.id = id; this.vehicle = vehicle; this.spot = spot; this.entryTime = Instant.now();
39 }
40 @Override public String getId() { return id; }
41 }
42
43 public static void main(String[] args) {
44 Vehicle car = new Vehicle("KA01AB1234", VehicleType.CAR);
45 ParkingSpot spot = new ParkingSpot("F1-C1", SpotType.COMPACT);
46 spot.occupy();
47 Ticket ticket = new Ticket(UUID.randomUUID().toString(), car, spot);
48 System.out.println(ticket.getId());
49 }
50}1from __future__ import annotations
2from abc import ABC, abstractmethod
3from dataclasses import dataclass
4from enum import Enum
5from datetime import datetime
6from uuid import uuid4
7
8class VehicleType(Enum):
9 BIKE = "BIKE"
10 CAR = "CAR"
11 TRUCK = "TRUCK"
12
13class SpotType(Enum):
14 BIKE = "BIKE"
15 COMPACT = "COMPACT"
16 LARGE = "LARGE"
17
18class Identifiable(ABC):
19 @abstractmethod
20 def get_id(self) -> str:
21 pass
22
23@dataclass(frozen=True)
24class Vehicle:
25 number: str
26 vehicle_type: VehicleType
27
28@dataclass
29class ParkingSpot(Identifiable):
30 id: str
31 spot_type: SpotType
32 occupied: bool = False
33
34 def get_id(self) -> str:
35 return self.id
36
37 def occupy(self) -> None:
38 if self.occupied:
39 raise ValueError("spot already occupied")
40 self.occupied = True
41
42@dataclass(frozen=True)
43class Ticket(Identifiable):
44 id: str
45 vehicle: Vehicle
46 spot: ParkingSpot
47 entry_time: datetime
48
49 def get_id(self) -> str:
50 return self.id
51
52if __name__ == "__main__":
53 car = Vehicle("KA01AB1234", VehicleType.CAR)
54 spot = ParkingSpot("F1-C1", SpotType.COMPACT)
55 spot.occupy()
56 ticket = Ticket(str(uuid4()), car, spot, datetime.now())
57 print(ticket.get_id())Money and Address.Use composition when the child dies with the parent. Use aggregation when the child survives. Use UUIDs for distributed creation. Use auto-increment IDs for simple database-backed systems.
Common failure modes:
"ACTIVE" or "PAID" instead of enums.Address, Money, or DateRange.Enrollment.