Named failures
Errors and domain exceptions
Error handling in LLD means naming the failure so callers can catch, log, and show the right message. Example: the lot is full and park throws RuntimeException("fail"). The gate app cannot tell full from a . SpotUnavailable is a name you can catch. InvalidTransition is how CLOSED tickets refuse to reopen.
Exception catalog
A is part of the API. Write the names on the whiteboard next to park and exit. HTTP status mapping is an edge concern — see REST API design. The lot throws types, not status codes.
| Type | When | Kind |
|---|---|---|
| SpotUnavailable | park when no fitting free spot | command expected failure |
| UnknownTicket | exit/pay/find-required with a bad id | command expected failure |
| InvalidTransition | close() from already CLOSED, or CLOSED→ACTIVE | illegal operation |
| LotClosed | park after hours if the story has hours | policy failure |
| IllegalArgumentException | blank plate at the factory — input, not domain plot | boundary validation |
Java: named failures
class SpotUnavailable extends RuntimeException {
SpotUnavailable() { super("no fitting free spot"); }
}
class UnknownTicket extends RuntimeException {
UnknownTicket(TicketId id) { super(id.toString()); }
}
class InvalidTransition extends RuntimeException {
InvalidTransition(TicketStatus from, TicketStatus to) {
super(from + " -> " + to);
}
}
class ParkingLot {
Ticket park(Vehicle vehicle) {
return allocation.choose(freeSpots(), vehicle)
.orElseThrow(SpotUnavailable::new);
}
Receipt exit(TicketId id) {
Ticket ticket = tickets.findById(id).orElseThrow(() -> new UnknownTicket(id));
return ticket.close(clock.instant(), pricing);
}
}User can cause it
Full lot, bad id, pay twice with a different amount. Named domain error. Not a bug.
You thought it impossible
Missed enum branch, null id in a constructor. Assertion-style failure. Fix the code.
Lookup miss
Queries return Optional. Commands that required the entity throw UnknownTicket. See nullability.
Throw vs Result vs null
Commands throw named errors or return a . Do not swallow them. Do not replace them with null — park() returning null when full looks like a missing ticket. Pick one, then keep it when the API evolves.
Checked vs unchecked in an interview
Make illegal states hard, then throw the rest
SQLException is not a domain error
Java: Ticket guards the transition
class Ticket {
private TicketStatus status;
Receipt close(Instant exit, PricingStrategy pricing) {
if (status != TicketStatus.ACTIVE) {
throw new InvalidTransition(status, TicketStatus.CLOSED);
}
status = TicketStatus.CLOSED;
return new Receipt(id, pricing.fee(this, exit));
}
}Mistakes
- throw new RuntimeException("error") — the message is not a contract. Callers will parse strings. Give the failure a type.
- Returning null on failure — park() returning null when full looks like a missing ticket. Throw or return a Result. Do not reuse null for three meanings.
- Catch and ignore — empty catch in exit “to keep the gate open” hides UnknownTicket. Fail loud; let the controller decide the message.
HTTP status is not the domain type
The controller may map SpotUnavailable to 409 and UnknownTicket to 404. ParkingLot still throws types. If park throws 409 as an int, the lot has learned HTTP. Keep the catalog on the domain; keep the status table on the edge. That is the same boundary as ParkRequest: the outside world has codes, the inside has names.
An untyped process(Object) method that returns a bag of ok/false and a string msg is an untyped error channel. Callers will switch on msg. Named exceptions (or a Result with a typed failure) are the class-API version of problem+json without dragging HTTP into the lot.
Java: Result if you refuse to throw on full
sealed interface ParkResult {
record Parked(Ticket ticket) implements ParkResult {}
record Full() implements ParkResult {}
}
class ParkingLot {
ParkResult tryPark(Vehicle vehicle) {
return allocation.choose(freeSpots(), vehicle)
.map(spot -> new ParkResult.Parked(openTicket(vehicle, spot)))
.orElseGet(ParkResult.Full::new);
}
}Do not swallow at the repository
Catching SQLException and returning null from findById turns infrastructure failure into “ticket missing.” The attendant refunds a car that is still in the lot. Translate or bubble; do not pretend the row was absent. UnknownTicket is for a successful query that found nothing. A down database is not UnknownTicket.
Exercises
- List three named exceptions for a parking lot. Which ones are thrown by park vs Ticket.close?
- Should find(TicketId) throw UnknownTicket or return Optional? Why is that a different kind of miss than exit?
- A repository throws SQLException. Where do you catch it, and what does ParkingLot’s signature look like?
Worked scene: full lot at the gate
park throws SpotUnavailable. The controller maps it to a “lot full” sign. A missed enum in allocation throws a different exception — that one pages an engineer. If both were RuntimeException("fail"), the sign and the pager look the same. Named types are how the edge decides. ParkingLot still has no HTTP status and no JDBC.
CLOSED→ACTIVE is InvalidTransition on Ticket, not a null status. Make the call hard to represent; throw when a caller still tries. Queries keep Optional. Commands that required the ticket throw UnknownTicket.
How to explain this in an interview
Lot full
SpotUnavailable (or ParkResult.Full). Never null Ticket. Never RuntimeException("fail").
Unknown id on find
Optional.empty(). The query succeeded; the row was missing.
Unknown id on exit
UnknownTicket. The command could not run.
SQLException
Adapter only. ParkingLot signatures stay clean of JDBC.
In about a minute, say: “SpotUnavailable when full, UnknownTicket when exit misses, InvalidTransition on CLOSED→ACTIVE, Optional on find. SQLException stays in the adapter. park still returns Ticket, not null.”
If they ask for Result instead of throw, pick one style and keep it when the API grows. Mixing null, Result, and exceptions for the same full-lot case is the smell process(Object) had with a string msg field.
An untyped process(Object) method returning ok/false is not a catalog. Named types are.
Takeaway
If the user can cause it, it is a named domain error — not RuntimeException("fail"). Queries use Optional. Commands that need the entity throw. SQLException never appears on ParkingLot. Make illegal states hard to represent; shout about the rest.