Who may begin, who may finish
Object ownership and lifecycle
Ownership and lifecycle mean deciding which object creates, updates, and retires each piece of state. Example: a car enters — who creates the ticket, who marks the spot occupied? At exit — who takes money, who closes the ticket, who frees the spot? If three classes all think they own that story, you get a double-free or a spot that stays occupied forever.
Ownership is a job, not a diamond
Ownership is not a UML decoration. It is a job. Someone creates. Someone holds. Someone closes. If those three jobs have three owners, the lifecycle leaks. A floor owns its spots for their whole life. The lot owns the floors. A ticket is issued by the lot — or by a factory the lot uses — because the lot is the thing that just allocated a spot. The ticket does not construct itself from a controller.
Closing is the same idea in reverse. Exit is a use case. A thin controller can start it. The lot releases the spot. The ticket moves to PAID then CLOSED. The spot stays in the floor. The ticket stays in the store as history. Destruction and “this stay is over” are different endings.
Four jobs on one rail
Name the for each job. If you cannot, the verb will float into a Manager.
- 01
Create
The owner, or a factory the owner calls. Not every controller.
- 02
Hold
The collection or field lives on the owner. Others get ids or copies.
- 03
Use
Associates may point at the object. They do not delete it.
- 04
End
Release, close, or destroy — pick one meaning, and one owner for it.
Park and exit as a life story
- Enter. The lot a ticket after a spot accepts the vehicle. Nobody else calls
new Ticket. - Stay. The repository the ticket. The floor holds the spot. The ticket only the spot.
- Pay. The ticket owns status. A policy computes Money. The ticket stores the fee. The lot does not setStatus.
- Leave. The lot releases the spot. The ticket . The spot is still there. The ticket row remains as history.
One issuer, one releaser
The methods below look small. That is the point. Creation and ending are named. The controller is not in the story except as a caller of park and exit.
class ParkingLot {
private final TicketRepository tickets;
private final SpotAllocationStrategy allocation;
Ticket park(Vehicle vehicle, Instant now) {
ParkingSpot spot = allocation.choose(freeSpots(), vehicle)
.orElseThrow(() -> new IllegalStateException("full"));
if (!spot.tryOccupy(vehicle)) {
throw new IllegalStateException("spot taken");
}
Ticket ticket = Ticket.issue(spot.id(), vehicle, now);
tickets.save(ticket);
return ticket;
}
Receipt exit(TicketId id, Instant now, PricingStrategy pricing) {
Ticket ticket = tickets.findById(id).orElseThrow();
Floor floor = floorOwning(ticket.spotId());
Money fee = pricing.feeFor(ticket.duration(now), ticket.vehicle());
ticket.pay(fee);
floor.release(ticket.spotId());
ticket.close();
tickets.save(ticket);
return new Receipt(ticket.id(), fee);
}
}Everyone calls new Ticket
Gate, controller, and lot all issuing ids is how you get two tickets for one spot. Creator belongs in one place. That is the same instinct as the Creator principle, said in parking-lot words.
No owner for the ending
If close() and release() are optional calls from whoever remembers, occupied spots leak. Make end-of-life a named job. Pair it with object lifecycle when status machines enter the picture.
Three kinds of ending
Release means the resource is free again: the spot can take another car. Close means the entity’s story finished: the ticket is history. Destroy means the object should not exist: the lot is demolished, floors go with it. Candidates mash these into delete(). Then a “deleted ticket” cannot be audited and a “deleted spot” cannot be reused. Name the ending you mean.
Release
Spot.free again. Seat available. Lock dropped. The thing remains.
Close
Ticket CLOSED. Booking CANCELLED. Keep the record. Stop transitions.
Destroy
Floor removed with the lot. Board cells gone with the game. Composition’s end.
Say the owner out loud
“ParkingLot issues tickets and releases spots. Floor owns spots. Ticket owns its status.” That sentence prevents half the bugs in park and exit. It also tells you what not to put on a webhook: a webhook may start exit. It must not new Ticket or setOccupied. Relationships that are only names belong in object relationships. Lists that must not leak belong in collections ownership.
Sessions, locks, and other short lives
The same four jobs apply to a cache entry, a rate-limit window, an elevator request, and a chess game. Someone creates the game. The board holds pieces. A clock is used, not owned by a pawn. The game ends without deleting the player accounts. If you cannot name those four owners, you will leak games, seats, or locks — the in-memory version of a resource leak.
A cache entry has a short, named life
Who creates the node? The cache. Who holds it? The cache’s map and list. Who uses it? Getters that read value. Who ends it? Eviction on the cache, not on the caller who happened to hold a reference. If caller.remove() deletes from the LinkedHashMap, LRU is a suggestion.
class LruCache<K, V> {
private final int capacity;
private final Map<K, Node<K, V>> byKey = new HashMap<>();
V put(K key, V value) {
Node<K, V> node = new Node<>(key, value);
byKey.put(key, node);
attach(node);
evictIfNeeded();
return value;
}
private void evictIfNeeded() {
while (byKey.size() > capacity) {
Node<K, V> oldest = detachOldest();
byKey.remove(oldest.key);
}
}
}Abandoned constructors in tests
Tests that call new Ticket(...) to skip park() will invent ids the lot never issued. Then exit cannot find them, or two tests share T-1. Construct through the owner in tests too, or use a factory the owner uses. Lifecycle is not only production code. A test that bypasses create is rehearsing the bug.
Ids are how non-owners point
Once you decide the owner, everyone else should carry an id. Ticket carries SpotId, not ParkingSpot, unless you are in a single-threaded toy and you accept the aliasing. Ids make “use without owning” visible in the type system. They also make persistence honest: you store a foreign key, not a nested copy you might delete by accident.
If two owners both hold the live object, you do not have two owners. You have a bug waiting for the first close(). Pick one holder. Give the other a name.
Rate-limit windows expire on purpose
Who creates the window? The limiter, when the first request arrives. Who holds it? The limiter’s map. Who uses it? allow() on the window or on the limiter. Who ends it? TTL expiry inside the limiter, not a request thread calling delete because it felt done. If callers end windows, you will drop a good window early or leak a stale one forever.
Locks and leases are the same four jobs. The store creates the lease. The leader renews. Followers do not delete the row; they wait for expiry. Ending is a named policy, not a courtesy call.
Write the owner on the diagram
Next to Ticket, write “issued by Lot, held by Repository, closed by Ticket, spot released by Lot.” That caption is the lifecycle. If two nouns appear as issuer, you are not done. Pick one before you code park().