Three different nothings
Null, Optional, empty collections
Nullability design means each absence has one clear meaning — and you encode it in the type, not with a shared null. Example: find(ticketId) returns null when the ticket is gone. freeSpots() returns null when the lot is empty. The first null is “this id is unknown.” The second should be a list of size zero. Callers who write spots.size() crash on the empty lot. Mixing those meanings is how an shows up in an interview.
Missing one entity
. Return or a typed error. The caller must decide: show “not found,” or throw in a command that required the ticket.
Optional<Ticket> find(TicketId)
Empty collection
Zero parked cars, zero free spots, zero search hits. An . The loop is the branch. Never null.
List<ParkingSpot> freeSpots()
Do-nothing policy
A : NoDiscount, SilentNotifier. Behavior that is allowed to be absent without a branch on every call. See the Null Object pattern.
Money fee(..., Discount d)
Decision tree (say it out loud)
- Is this a collection the caller will iterate? Return a list. Empty is valid. Null is not.
- Is this one entity that might not exist? Optional for queries. Named exception for commands that required it (exit needs a ticket).
- Is this a strategy that might be “none”? Null Object, not Optional<Discount> on every fee line.
- Is this a programmer bug? requireNonNull in constructors. Do not let Ticket exist with a null id.
Java: three different nothings
class ParkingLot {
Optional<Ticket> find(TicketId id) {
return tickets.findById(id); // missing one
}
List<ParkingSpot> freeSpots() {
return List.copyOf(free); // maybe empty, never null
}
Money fee(Ticket ticket, Discount discount) {
return discount.apply(baseFee(ticket)); // NoDiscount, not null
}
}
final class NoDiscount implements Discount {
public Money apply(Money base) { return base; }
}Be consistent in one API
If find returns Optional, do not also return null from findByPlate. Pick Optional for lookups, exceptions for illegal commands, empty lists for searches. That policy is part of class API design and error handling. SQL NULL in a column is a storage concern; map it at the repository. The domain Ticket should not have three nullable strings “because the table does.”
Null collections
freeSpots() returning null means every for-loop needs a guard. Return empty. The caller’s loop just runs zero times. This is also a collections ownership rule: copy, do not share, do not null.
Optional wrapping a collection
Optional<List<Ticket>> has two empties. Callers will get() and then null-check. Use List. Use Optional only for a single missing value.
Java: command vs lookup
class ParkingLot {
// Query — absence is a value
Optional<Ticket> find(TicketId id) {
return tickets.findById(id);
}
// Command — absence is a failure of the use case
Receipt exit(TicketId id) {
Ticket ticket = tickets.findById(id).orElseThrow(() -> new UnknownTicket(id));
return ticket.close(clock.instant(), pricing);
}
}Optional is not a field strategy
Putting Optional<Instant> exitTime on Ticket and forgetting to unwrap it is how NPEs move into the entity. Prefer a state machine: OPEN tickets have no exit; CLOSED tickets have an Instant. Illegal CLOSED→ACTIVE is a typed error, not a null exit time. See state vs status.
HLD cousin: nullable columns
Tables use NULL for unknown columns. That is not permission for Ticket to be a bag of nulls. Map at the repository: a NULL exit_ms becomes Optional.empty() or a CLOSED vs OPEN type. Schema evolution lives in data modeling and schema evolution; the LLD rule is still “empty list vs missing one vs no-op policy.”
requireNonNull is not Optional
Constructors use Objects.requireNonNull because a Ticket with a null id is a bug, not a lookup miss. Optional is for answers that may be absent. requireNonNull is for values that must exist for the object to be well-formed. Mixing them — Optional<TicketId> id on Ticket — makes every method unwrap an identity that should never have been missing.
An untyped process(Object) method returns Object, which is often null for “full,” “missing,” and “empty list.” That is three meanings in one return. park(Vehicle) returning Ticket, find returning Optional, freeSpots returning List is how you stop encoding nothing as null. The class API and nullability are the same design viewed from names and from absence.
Java: never Optional of a list
// Wrong — two empties
Optional<List<Ticket>> openMaybe();
// Right — one empty
List<Ticket> openTickets();
class ParkingLot {
List<Ticket> openTickets() {
return List.copyOf(tickets.findOpen()); // empty list if none
}
}Query miss
The attendant looks up a plate. Empty Optional is a valid answer. The UI shows “not in the lot.”
Command miss
The driver presents a ticket that was never issued. exit cannot complete. UnknownTicket is the API. Do not return a null Receipt.
Exercises
- A teammate returns Optional.empty() from freeSpots when the lot is full. Rewrite the signature and the one-line reason.
- exit(TicketId) when the ticket is missing: Optional or UnknownTicket? Why is this a command, not a query?
- Discount may be “none.” Why is NoDiscount better than passing null into fee()?
Worked scene: the attendant’s loop
for (ParkingSpot s : lot.freeSpots()) draw(s). If freeSpots returns null when the lot is packed, the loop never starts — it NPEs. If it returns empty, the canvas is blank, which is correct. find(id) in the same UI uses Optional: no ticket card, not a crash. fee(..., NoDiscount) never branches. Three widgets, three nothings, zero shared null.
An untyped process(Object) method that returns null cannot tell the UI which widget to blank. Typed absences can. That is API design viewed from the empty case.
How to explain this in an interview
Missing one ticket
Optional on find. UnknownTicket on exit. Not a null Receipt.
Zero free spots
Empty list from freeSpots(). Packed lot is not a missing list.
No discount
NoDiscount object. fee() does not null-check.
Null id in constructor
requireNonNull. That is a bug, not Optional<TicketId>.
In about a minute, say: “find returns Optional. freeSpots returns an empty list, never null. NoDiscount is a real object. I will not wrap a list in Optional. An untyped process(Object) method returning null meant three things; these types mean one each.”
Commands that required the ticket throw UnknownTicket. Lookups stay Optional. Packed lots are empty collections, not missing collections.
Java: empty list vs missing ticket
List<ParkingSpot> freeSpots() {
return occupied == capacity ? List.of() : List.copyOf(free);
}
Optional<Ticket> find(TicketId id) {
return tickets.findById(id);
}Takeaway
A missing ticket is not the same as zero parked cars, and neither is a do-nothing discount. Optional for one absent entity, empty lists for collections, Null Object for policies. Never return null for something the caller will iterate.