Low-level design
From a paragraph of requirements to a class list you can defend
Requirements-to-classes means turning a messy paragraph into use cases, entities, value objects, and verb owners — without dumping every method on ParkingLotManager. For example, “vehicles enter, get a ticket, leave, and pay by time and vehicle type” should become a short list of things with identity and a shorter list of actions that need homes.
We have a talking order and a tiny API. Now the owner hands you that paragraph. Do not start with a manager class. Start with nouns and verbs. This page is that translation.
Translation pipeline
A path from sentence to types, not a leap to a Manager
Your first class list is a guess. After you assign jobs, some nouns become values. Some verbs become strategies. ParkingLotManager should have disappeared. If it is still there with twenty methods, you stopped too early.
1
Use cases
Park, exit, pay — verbs you can say.
2
Things + values
What lasts vs what is just an amount.
3
Who does each verb?
Give the job to the object with the data.
4
Small interfaces
Chooser, pricing, store — then code.
Requirement → use cases → things and values → verb homes → who talks to whom → small interfaces → code
Underline the paragraph for nouns, values, and verbs
The parking problem says: vehicles enter, get a ticket, leave, and pay by time and vehicle type. Underline nouns that last over time. ParkingLot. Floor. ParkingSpot. Vehicle. Ticket. Those are things you can point at later. They usually become classes with an id — .
Underline values that should not have an id. Money. Duration. VehicleType. SpotType. Twenty rupees is not “that twenty.” Any other twenty of the same currency is the same value. Keep those small and hard to mix up. That is a .
Underline verbs: allocate, release, calculateFee, issueTicket. Ask which noun is responsible for each verb. Allocate needs a view of free spots. That lives near the lot, or near a small “how we pick a spot” type the lot uses. CalculateFee needs entry time, exit time, and a policy. That is not the spot’s job. IssueTicket is creation. The object that records tickets is a natural .
Worked underlining
Vehicles enter a parking lot with several floors of spots, get a ticket, leave, and pay by time and vehicle type. Two gates must never give out the same spot.
Sky: things with identity
Green: values
Amber: verbs that need a home
Not every noun becomes a class
“Customer name” may not need a type. Money and VehicleType do. is how fee bugs leak into every file. Skip nouns that are just labels. Keep nouns you will look up later. Promote numbers that have rules.
| From the paragraph | First cut | Why |
|---|---|---|
| Parking lot, floor, spot | Entities | You point at them later; they have lifecycle. |
| Vehicle, ticket | Entities | Plate and ticket id survive field changes. |
| Vehicle type, spot type | Enums / values | No identity; they classify other things. |
| Money, duration, time | Value objects | Rules (non-negative, same currency) belong here. |
| Gate | Actor, not a class yet | May become a thread or a controller, not a domain noun. |
| Customer name | Skip or a field | You are not building a CRM today. |
| Pay / allocate / issue | Verbs → methods or strategies | Need a home, not a class named Pay. |
A first class list, not a Manager
Write this on the board before you implement. It is allowed to be wrong. It is not allowed to be one class with every method. Domain modeling will refine who allocates. This list is the honest first cut.
// things you can point at later
class ParkingLot { }
class Floor { }
class ParkingSpot { }
class Vehicle { }
class Ticket { }
// values — no identity of their own
enum VehicleType { CAR, BIKE, EV }
enum SpotType { CAR, BIKE, EV }
final class Money {
private final long paise;
private final String currency;
Money(long paise, String currency) {
if (paise < 0) throw new IllegalArgumentException("paise");
this.paise = paise;
this.currency = Objects.requireNonNull(currency);
}
Money plus(Money other) { /* same currency, return new Money */ }
}
// verbs that need a home
interface SpotAllocation {
Optional<ParkingSpot> choose(Vehicle vehicle);
}
interface Pricing {
Money fee(Ticket ticket, Instant exitTime);
}
interface TicketBook {
Ticket issue(Vehicle vehicle, ParkingSpot spot, Instant at);
Ticket require(TicketId id);
void close(Ticket ticket);
}Entity
Identity survives field changes
If I change the spot on this ticket, is it still the same ticket? Yes. Ticket T-1042 is still T-1042 if the car moves. Compare tickets by id, not by fee.
Put occupy on the spot because the spot is the for occupancy. A service that sets spot.free = false from the outside is how the invariant leaks.
Value
Replace, do not mutate
If I change the paise on this Money, is it still the same money? No. Throw the old one away. Values should be immutable so two threads can share a fee policy without surprise.
Read entity versus value object when this distinction starts leaking into equals().
Give each verb a home, then watch the Manager shrink
Nouns without jobs are empty boxes. Walk the verbs. If the only answer is still ParkingLot, you have not assigned yet. A thin ParkingLot that delegates is fine. A ParkingLot that prices, persists, and notifies is a God object again.
class ParkingSpot {
private final SpotId id;
private final SpotType type;
private Vehicle occupant;
boolean canFit(Vehicle vehicle) {
return occupant == null && type.accepts(vehicle.type());
}
void occupy(Vehicle vehicle) {
if (!canFit(vehicle)) throw new IllegalStateException("cannot occupy");
occupant = vehicle;
}
void vacate() {
occupant = null;
}
}
class NearestEmptyAllocation implements SpotAllocation {
private final List<Floor> floors;
public Optional<ParkingSpot> choose(Vehicle vehicle) {
return floors.stream()
.flatMap(floor -> floor.spots().stream())
.filter(spot -> spot.canFit(vehicle))
.findFirst();
}
}
class TicketBookInMemory implements TicketBook {
private final Map<TicketId, Ticket> byId = new HashMap<>();
public Ticket issue(Vehicle vehicle, ParkingSpot spot, Instant at) {
Ticket ticket = Ticket.open(TicketId.newId(), vehicle, spot, at);
byId.put(ticket.id(), ticket);
return ticket;
}
}Allocate lives on a chooser that can see free spots. Occupy lives on the spot. Issue lives on the book that will look the ticket up later. CalculateFee will live on Pricing, which you already declared. The remaining ParkingLot (or ParkingService) only walks these objects. That walk is orchestration, not a God class.
The God object, written down so you can refuse it
ParkingLotManager.park, .price, .save, .notify is a script with a class name. Split the verbs until the Manager is thin or gone. If you catch yourself adding a twentieth method, stop and ask which noun should have received that method instead.
// refuse this shape
class ParkingLotManager {
Ticket park(Vehicle v) { /* find, occupy, issue, sms, metrics */ }
Money price(Ticket t) { /* switch on type, then on hour, then on coupon */ }
void save(Ticket t) { /* JDBC */ }
void notify(Ticket t) { /* email */ }
void addFloor(Floor f) { }
void reportDaily() { }
}
// prefer this shape: the lot is a walk, not a dump
class ParkingLot {
Ticket park(Vehicle v) {
ParkingSpot spot = allocation.choose(v).orElseThrow(LotFullException::new);
spot.occupy(v);
return tickets.issue(v, spot, clock.instant());
}
}After you assign, the list will change. That is the point.
Gate looked like a noun
Pay looked like a class named PaymentService
Every noun became a class, every number stayed a primitive
You named Strategy before you named the owner
Try the same method on a second paragraph
“People come in, take a token, wait, and pay by item and size.” Nouns that last: Order, Token, Item. Values: Size, Money. Verbs: takeOrder, bake, callNumber, price. Who prices? Not the token. Who issues the token? The object that will look it up when the number is called. A single God class that does every job should not survive that paragraph any more than ParkingLotManager survives the lot.
interface BakeryCounter {
Token takeOrder(List<LineItem> items);
Order collect(Token token);
}
final class LineItem {
private final Item item;
private final Size size;
private final int qty;
}
interface Pricing {
Money total(List<LineItem> items);
}
interface Kitchen {
void bake(Order order);
boolean ready(Token token);
}Where to go once the list is honest
- Domain modeling — who should allocate a spot, and who must not.
- Responsibility assignment — GRASP-style homes for each verb.
- How to run the round — this translation sits after the API nod and before you type park.
- Parking lot case — the list, fully implemented.
Takeaway
Underline nouns that last, values that should not have ids, and verbs that need a home. Ask who has the data. Ask who will look the thing up later. Let the Manager disappear. The first list is a guess; the assigned list is a design. Next we look at what interviewers actually mark besides “used a pattern,” and why silent perfect code still reads as weak design.