API, persistence & data modeling
Designing a class API
Class API design means naming typed methods for each use case — not one untyped funnel. Example: the interviewer asks for a parking lot. One candidate writes Object process(Object request). Another writes Ticket park(Vehicle) and Receipt exit(TicketId). The second design is already teaching the domain; the first postpones every decision.
The is the system operations. Names are verbs from the . Arguments are domain types, not Object, Map, or a JSON string. are things the caller will need next.
Grade this first
Verb from the story
park, exit, pay — not process.
Typed arguments
Vehicle, TicketId — not Object.
Useful returns
Ticket, Receipt, Optional.
No internals
Spots and SQL stay behind the lot.
Weak surface
One funnel, no types
process(Object) hides every decision. The compiler cannot help. The next method cannot be named because there is only one funnel. You cannot test “exit a missing ticket” without inventing a map key the method happens to understand.
Strong surface
Commands named after the story
park(Vehicle) is a . exit(TicketId) is another. find(TicketId) is a . You can talk about full lots, unknown tickets, and retries without decoding a blob.
What the public methods are for
In HLD you sketch HTTP paths. In LLD the same idea lives on a class: a small, named surface. The REST API design note is the network cousin of this page. Here the “client” is another object in the same process — a gate, a kiosk, a test.
| Signature | Kind | Why this shape |
|---|---|---|
| Ticket park(Vehicle vehicle) | Command | Allocates a spot; caller needs the ticket id to print. |
| Receipt exit(TicketId id) | Command | Closes the stay; caller needs the fee. |
| Optional<Ticket> find(TicketId id) | Query | Lookup may miss. Empty is not a crash. |
| int occupiedCount() | Query | Derived from state. Safe to call twice. |
Keep the surface small
ParkingLot does not need printReport, sendSms, and connectDb on day one. Add a method when a use case needs it. Prefer an or a new type over a boolean that changes meaning. The parking lot case is the running example for this whole section.
A fat API is how God objects start. Every extra method is a reason to change the class. If the kiosk needs a receipt reprint, that is a query on an already closed ticket — not a second exit.
Java: use cases, not a blob
Write the signatures even if the bodies are empty. Filling HashMaps comes after the names are honest.
// Weak — one funnel, no types
class ParkingLot {
Object process(Object request) {
// park? exit? pay? the map keys decide
return null;
}
}
// Strong — commands named after the story
class ParkingLot {
Ticket park(Vehicle vehicle) { /* allocate, then save */ }
Receipt exit(TicketId id) { /* close, price, free the spot */ }
Optional<Ticket> find(TicketId id) { /* query — no writes */ }
}What not to return
Live collections
return tickets.values() lets a caller clear() your store. Return a copy, a page, or an unmodifiable list. The lot owns the collection.
Storage types
Connection, ResultSet, and EntityManager on ParkingLot are a persistence leak. The lot talks to a repository port. JDBC stays in the adapter.
Layout types
ParkingSpot[] from park() couples the gate to floors and sizes. Return Ticket. The spot id can live on the ticket if the receipt needs it.
That is an API . The follow-up pages on persistence boundaries and input and output models are the same rule at different API boundaries.
Interview sequence
Write the API before the map
- 1List the actor verbs: park, exit, find, maybe pay.
- 2Name arguments as domain types: Vehicle, TicketId — never Object.
- 3Pick returns: Ticket, Receipt, Optional, empty list. Not null for collections.
- 4Refuse internals: no Connection, no live HashMap, no ParkingSpot[].
- 5Then implement. The HashMap is an adapter, not the design.
Flags, overloads, and meaning
park(vehicle, true) — true what? Charger? Valet? Handicap? A flag is not an API. When the story grows, add ParkOptions or a second and keep the original method doing what it did. That habit is API evolution: extend the contract; do not silently change it.
class ParkingLot {
Ticket park(Vehicle vehicle) {
return park(vehicle, ParkOptions.defaults());
}
Ticket park(Vehicle vehicle, ParkOptions options) {
// charger / valet flags live here, not as a nameless boolean
}
}Mistakes that fail the round early
process(Object) / handle(Map)
If the signature does not name the verb, the design is not done. Write park(Vehicle) even if the body is empty. Fill the body next.
Leaking internals
Returning ParkingSpot[] or Connection from park() couples callers to storage and layout. Return Ticket. Keep spots and SQL behind the lot.
void park with a side-channel
Stashing the last ticket on a getter the caller “just knows about” is a hidden return type. Return Ticket from park.
One method per HTTP verb cargo-cult
You do not need create/read/update/delete on ParkingLot. You need the story’s verbs. CRUD is a database shape, not a parking lot.
How this pairs with commands and queries
Naming park and exit is half the job. The other half is not letting find write. Read commands vs queries next, then nullability so find returns Optional instead of a surprise null. In system design, “clients retry the same call” usually means idempotency. In LLD, the matching idea is making pay safe to send twice.
Exercises
- Rewrite a class that only has process(Map) into park, exit, and find. What types did you invent just to make the signatures compile?
- A teammate wants park to return the ParkingSpot so the UI can draw the floor. What do you return instead, and where does the floor number live?
- List three methods you would refuse to put on ParkingLot on day one. Which class would own each?
Takeaway
Interviewers grade the API before they grade the HashMap. park(Vehicle) and exit(TicketId) are the design. process(Object) is a postponement. Keep the surface small, typed, and free of JDBC and live collections.