Archive Is a Visibility State, Not a Lifecycle State

concurrency data-modelling product-decisions state-modelling system-design Sep 08, 2026

When a workspace fills up with items nobody is working on any more, the fix looks like a new status. It usually isn't. Archiving is a decision about what someone wants to see, and modelling it as a lifecycle status quietly corrupts the meaning of the status field for everything that reads it.

The problem was noise, not a missing stage

A triage workspace, where every item a user owned stayed in "Active" indefinitely. There was no stage after the work was done — items just accumulated, and the list got steadily less useful. The complaint arriving from users was "I can't find anything," which reads like a missing workflow stage. Add a "Completed" or "Closed" status, filter the list on it, done.

That framing is wrong in a specific and expensive way. The status field described where the item had actually got to in a real process, and its value was derived from the item's own contents and history. "I don't want to look at this any more" is not a fact about the process. It is a fact about the person looking. Putting it in the same field means the status field now answers two questions with one value, and no consumer can tell which one it is being told.

Three kinds of state

Before adding a new state anywhere, it is worth deciding which of three things it is.

Orthogonal-visibility state. Controls what a viewer sees, and nothing else. It can be set and unset freely, it does not constrain which actions are legal, and it makes no claim about the work itself. Archive, pin, favourite, mute, snooze.

Lifecycle-authoritative state. Makes a claim about where a thing has actually got to. Other parts of the system are entitled to trust it — to decide which transitions are permitted, what is safe to delete, what gets counted or billed. Often derived, and usually should stay derived.

Access-controlling state. Determines who may see or act on something. It belongs with authorization, it must be enforced on the server, and it is never a display concern.

These fail in different ways, and that is the whole reason to keep them apart. A wrong visibility state is annoying. A wrong lifecycle state produces incorrect decisions downstream. A wrong access state is a breach. Collapse them into one enum and every consumer of that enum inherits the weakest guarantee of the three.

The tell that you are about to overload something: you want to add a value to a derived status purely to make a list shorter.

The shape of the fix

Archiving became an additive, nullable timestamp on the item. Null means not archived; a value means archived at that moment. It is a small choice that does a lot of quiet work.

  • The migration is additive and needs no backfill. It does not try to infer archive intent from history. Every existing row is already correct, because nothing was archived — archiving did not exist yet.
  • Restore is clearing the field. Reversibility is structural rather than a second feature bolted on later.
  • A timestamp rather than a boolean costs nothing and gives you "when" for free.
  • The existing status keeps its meaning. Nothing that consumed status had to learn a new value or a new exception.
  • The list view partitions into Active and Archived, and that partition stays a read concern. Visibility never became a precondition for anything else.

The last point is the one that matters most a year later. Because archive lives in its own field and means only one thing, no future feature has to ask whether an archived item is "really" finished.

The one place visibility touches behaviour

There is a seam where a visibility decision does collide with real work: an item can be archived while a long-running background job for it is in flight. If archiving out from under active work is not allowed, then a disabled button in the interface is not the mechanism. It is a courtesy to the user, and nothing more.

The real mechanism is that archiving and claiming a job contend for the same row lock. This is the principle I have written about elsewhere as Same Invariant, Same Lock: if two operations must not both succeed, they have to serialize on the same thing, not on two things that usually agree. Archiving takes the same item-row lock that job claiming takes, and refuses only when a fresh job is being started.

Note how narrow that refusal is. Archive does not cancel, settle, reclaim, or otherwise mutate the work in flight. A visibility feature has no business mutating work — the temptation to make archive "tidy up on the way out" is exactly how a small feature grows a blast radius. It declines, and it says why.

The trade-offs left standing

A few things were deliberately not solved, and naming them is part of the work. "We shipped something small and knew what we had not solved" is a different position from "we shipped something small."

  • Archived rows are still sent to the browser; the partition happens on the client. That is fine at current volumes and stops being fine once archived items materially outnumber active ones. Server-side scoping or pagination belongs before that point, not after.
  • Archiving updates the item's modified timestamp and shows up in its activity record, so a pure visibility action registers as activity. That is a small lie about the item, tolerable for now.
  • There is no discoverability beyond the Archived tab — no search across archived items, no way to stumble back onto one.

None of these are worth fixing on speculation. They are worth writing down, so that when real usage warrants a change, it is a revisit rather than a discovery.

How to apply it

  1. Before adding a state, classify it: orthogonal-visibility, lifecycle-authoritative, or access-controlling. Write the classification down where the next person will find it.
  2. If it is orthogonal, keep it orthogonal. Its own field, independently settable, carrying no meaning for any other subsystem.
  3. Resist adding values to a derived status to reduce triage noise. Noise is a view problem, and view problems are cheap to fix in the view.
  4. Prefer an additive nullable field over a backfill. The absence of a value is usually the honest answer for historical rows.
  5. Where a visibility action can collide with in-flight work, name the exact seam and serialize on it. Do not rely on the interface to prevent it.
  6. Record the trade-offs you accepted and the signal that would make each one worth revisiting.

Archive is one of the smallest features a workspace can grow, and one of the easiest to model badly. Getting it right is mostly a matter of refusing to let a display preference become a claim about the work.