Behavioral modeling
Modeling object lifecycle
Object lifecycle is the story of how an entity is born, changes status, and ends — and which type is allowed to drive each step. A ticket is not a bag of fields; it is a timeline of allowed moves.
Example: someone creates the ticket, activates it when the car parks, takes payment, and closes it when the gate lifts. If “someone” is always ParkingService and Ticket is only fields, the lifecycle is invisible and bugs hide in service methods.
Biography timeline
Birth
01Factory / Lot
Ticket issued as CREATED. A stall is already chosen.
Activate
02park
CREATED → ACTIVE. Spot occupied. Clock starts.
Pay
03exit / pay
ACTIVE → PAID. Strategy priced; entity accepted.
Death
04gate / close
PAID → CLOSED. Spot released. Row may remain.
Ticket lifecycle Factory / Lot ──creates──► CREATED park ──activate─► ACTIVE (spot occupied) exit / pay ──pay──────► PAID gate ──close────► CLOSED (spot released) CLOSED is the end. The row in the repository may stay. The car does not. The spot is free for someone else. Nobody resurrects CLOSED → ACTIVE.
Birth, chapters, death
is creation — usually the lot or a factory, because they are there when a spot is taken. The are CREATED, ACTIVE, PAID, CLOSED. is CLOSED. A closed ticket is a record, not a vehicle that can park again.
Name the only objects allowed to move each chapter. Factory (or lot) creates. Spot occupancy and ticket activation should happen together in park. Pay and close belong to exit. A webhook, a retry, or an admin “fix” still has to call those commands. Nobody gets a back door that resurrects CLOSED.
Leftover flags — parked, billed, gone — are lifecycle you refused to name. Name the chapters, pick a creator, pick a closer, and the flags disappear into one status the entity owns. The arrows themselves are the state machine. This page is about who is allowed to turn the pages.
Ownership of parts
: a lot owns floors. Destroy the lot and the floors go with it. That is ownership lifecycle— a different question.
Biography of a stay
A ticket is associated with a spot id. Closing the ticket must not delete the stall. Mix the two ideas and you get a ticket destructor that wipes concrete.
Who may start it, who may finish it
Factory creates. Controller sequences park and exit. Entity refuses illegal chapters. Repository rehydrates and saves; it does not close. That is the pocket sort from assigning responsibilities applied to a life, not to a fee.
class Ticket {
private final TicketId id;
private final SpotId spotId;
private TicketStatus status = TicketStatus.CREATED;
static Ticket issue(Spot spot, Vehicle vehicle) { // birth — factory / lot
return new Ticket(TicketId.newId(), spot.id(), vehicle);
}
void activate() { /* CREATED → ACTIVE — park */ }
void pay(Money fee) { /* ACTIVE → PAID — exit */ }
void close() { /* PAID → CLOSED — death of the stay */ }
}
class TicketRepository {
Ticket get(TicketId id); // rehydrate a record
void save(Ticket ticket); // remember — do not close
}
// Factory that also calls close() stole the gate.
// Repository that sets CLOSED stole the entity.Keep-out list for a life
- Repository saves; it does not close and it does not activate.
- Strategy prices; it does not issue tickets.
- Factory creates; it does not take payment or lift the gate.
- A webhook still calls pay() or is ignored. It never assigns ACTIVE onto CLOSED.
Flags are a biography you refused to write
parked=true, paid=false, closed=true is not a biography. It is three independent lies. One status, owned by the entity, is the life. The Creator principle answers who news the ticket up. This page answers who may walk it to CLOSED and who may not walk it back.
Ask at birth
Who creates Ticket, and what must already be true (a spot occupied)?
Ask at death
CLOSED is terminal. The ticket remains as a record; the car is gone; the stall is free.
Common mistakes
Factory that also closes tickets
Creation is one job. Closing is another. A factory that knows about payment and gates is a God object with a creational name.
Lifecycle as leftover flags
parked=true, paid=false, closed=true is not a biography. It is three independent lies. One status, owned by the entity, is the life.
Closing a ticket deletes the stall
Association is not composition. Death of the stay releases the stall. It does not demolish it. See object relationships.
Rehydrate is not birth
The repository can load a CLOSED row from last Tuesday. That is memory, not a new stay. Birth is Ticket.issue when a stall is taken. If get() secretly calls activate(), you invented a second birthday. Tests should create with issue(), walk the chapters with commands, and use the repository only to prove the row survived.
Anemic tickets — public fields, no commands — have no biography. The service then becomes the parent, the doctor, and the undertaker. That is how CLOSED → ACTIVE sneaks in as a field write. Keep the life on the entity even when the row is old.
May create
Factory, Lot — they saw the stall taken.
May turn pages
Controller calls activate, pay, close. Entity accepts or throws.
May only remember
Repository. No chapter changes. No second birth.
Say the biography in one breath
“Lot creates the ticket when occupy succeeds. Park activates. Exit pays and closes. CLOSED is the end; the stall lives on. Repository stores the record.” If you cannot say who is allowed to finish it, you do not have a lifecycle yet — you have a DTO.
Spot has a shorter life
FREE → OCCUPIED → FREE is a cycle. Ticket is not. Do not copy Ticket’s terminal CLOSED onto a stall. The stall returns to FREE when the stay dies. Two biographies, two tables, one park use case that must move them together. If occupy succeeds and activate fails, you have a stall with no ticket — roll both back in the same controller method. Birth and occupy are one scene; do not leave a half-born stay.
Read next
Takeaway
Lifecycle is birth, chapters, and a death that cannot be undone. Factory creates. Park activates. Exit pays and closes. CLOSED is terminal. Repository remembers; it does not resurrect. Do not confuse composition ownership with the biography of a stay.
Spot cycles FREE → OCCUPIED → FREE. Ticket ends at CLOSED. Two lives, one park.