Lifecycles
A lifecycle is a value that moves through states over time. It can be a status enum, a set of nullable timestamps, or booleans that together mean a state. These rules apply to the server. ADR 0078 records why we chose them. Some code that is older than ADR 0078 does not obey all the rules yet. Do not copy it.
Add a lifecycle
- Use one enum column for the state. Do not hide a state in a JSON key or in a combination of booleans.
- Add a CHECK for the closed set of values, in the same migration as the column.
- Add a CHECK for each column that a state implies, such as a reason or a completion time.
- Put the legal transitions in one table beside the enum.
IntegrationStateshows the form. - Write each transition as a compare-and-set on the expected state, as
FeedbackSendRepository.claimdoes. - Refuse an illegal transition, and record the reason.
- Decide what a crash, a replay and a retry do to each state. State the answer in a test.
Use a plain enum. Do not add a state machine framework.
Choose the rigor
Rank a lifecycle by the damage of a wrong transition: trust, tenancy, data loss, then stuck work. The levels add up. Levels 1 to 3 follow the rank. Levels 4 and 5 apply only where their condition holds.
| Level | Add | When |
|---|---|---|
| 1 | CHECK constraints | Each stored lifecycle |
| 2 | A transition table and table-driven tests | More than one writer |
| 3 | Seeded random walks over events | A wrong transition costs trust, crosses a tenant, or loses data |
| 4 | Optional: a small TLA+ model, outside CI | A concurrent protocol that is hard to reason about: a lease, a permit or a fence |
| 5 | An invariant query with a metric and an alert | A rule across tables, which a CHECK cannot express |
Close what you own
Most defects sit where one lifecycle hands over to another, not inside one. When a parent ends, end its children with the parent's reason. Do this in the same transaction inside one module, and through a durable event across modules (Transactions and side effects). Do not leave a child in a state that only an active parent can move on. Do not report a child as finished when nothing measured it.
Repair old rows first
A CHECK validates each existing row. Rows from before the 1.0 cutover can break a rule that today's code keeps. Count them on staging and in production first. Then repair them in the same migration.