ADR-0001: Lifecycle status is an explicit enum, not derived at runtime
- Status: Accepted
- Date: 2026-05-30
- Scope: core (any backend with domain lifecycles)
Context
Domain entities with a lifecycle (in Expenses: invoices, installment series,
subscriptions, …) need to express what state they’re in. It is tempting to derive that state
at display time from runtime conditions — e.g. treating PARTIALLY_PAID && cycleEnd < today
as “closed-and-partial”, or computing “refinanced” from related rows. Deriving
status this way couples display logic to date semantics, makes persistence
queries awkward (you can’t filter on a state that doesn’t exist as a column),
and lets the same status mean different things in different views.
Decision
When the domain gains a meaningful new lifecycle state, encode it as a
status enum value with explicit transitions on the entity. Do not infer it
from runtime conditions.
- New distinction the user describes → a new enum value + explicit transition
method on the entity (intent-expressing, not a raw setter). - Backfill existing rows via a Flyway migration so historical data matches the
new model. - Reserve runtime checks only for things the enum genuinely doesn’t carry —
e.g. “is this overdue right now”, which is a function of today’s date and
needn’t be persisted.
Consequences
- Status means the same thing in every view and is directly queryable.
- One migration per new state (accepted cost; keeps data honest).
- Pairs with the broader DDD stance that entities own their invariants through
intent-expressing methods rather than exposing setters.
Related
- 0003-ddd-intent-methods — the entity-owned transition methods this pairs with
- Origin (Expenses): the user rejected a
PARTIALLY_PAID && cycleEnd < todayruntime scheme
— “if most of the invoice stats depend on dynamic and runtime conditions, then
they are useless… we should have clearness as much as possible.”