Key takeaways
- Use stable identifiers that do not carry editable meaning.
- Write invariants beside fields, not only in validation code.
- Distinguish unknown, absent and empty values.
- Decide how old records cross the next shape change.
Code makes a weak data decision expensive. Before behaviour exists, a field can be renamed with a pencil stroke. After records exist, the same change can affect validation, queries, displays, checks and migration. So sketch representative records early. Annotate them ruthlessly.
Our invented example is a reading-session request. A person asks for a room and a time window. A coordinator later makes a decision. This is not a real service or a recommended architecture; it is small enough to expose the choices.
Choose identity without smuggling in meaning
A stable identifier should survive edits. A room label is a poor request identifier because labels can change and collide. A sequence may reveal volume or become awkward when records merge. An opaque generated value usually works for a small project, provided uniqueness is enforced rather than assumed.
Keep human-facing reference text separate. A short reference such as “RQ-184” can help conversation, but it need not be the storage key. If reference generation fails, the durable identity still stands.
Annotate one representative record
Original annotated record
request = {
id: "opaque-identity", // immutable; unique
owner_id: "person-identity", // required; stable owner
room_code: "north-small", // closed known set
starts_at: recorded_time, // includes offset
ends_at: recorded_time, // later than starts_at
note: null, // absent, not unknown
status: "awaiting", // allowed state only
decision_reason: null, // required after decline
shape_revision: 1 // migration boundary
}Comments explain rules the syntax cannot. The end must be later than the start. The room code belongs to a closed set. A declined record requires a reason. These are invariants: conditions that must remain true after every accepted operation, not merely when a form happens to be used.
Notice the recorded time includes an offset. Time handling gets messy quickly. If a project crosses clock changes or regions, “09:00” alone is insufficient. The exact representation depends on the system, but the conceptual requirement should be settled before display formatting distracts everyone.
Give absence one meaning at a time
Nullable fields are not bad; ambiguous nullable fields are. Here, note: null means the requester supplied no note. An empty string should be normalised to the same meaning or rejected, rather than creating two versions of “nothing”. For a decision reason, null means no decision requiring a reason has occurred yet.
Unknown is different. If an older imported record may or may not have contained a note, null would falsely assert absence. That case may require an explicit note_status or a migration rule. Do not create extra states without a real need, though. Precision has a maintenance cost.
Make ownership executable
An owner_id is not merely metadata. It supports rules: only the owner may withdraw an awaiting request; coordinators may decide but cannot silently transfer ownership. If a record can belong to a group, say whether the group owns it or whether one person acts on the group's behalf. Both at once invites inconsistent access checks.
Ownership also affects deletion. If a person leaves, does the request disappear, become anonymous, or remain linked to a retained identity? The answer touches privacy and operational history. A tiny learning build can choose deletion with all records, but it should say so.
Draw the migration boundary
shape_revision: 1 does not perform a migration. It gives later code a fact to inspect. Suppose revision 2 separates one time window into date, start and duration. Existing records need a deterministic conversion, rejection or declared reset. “The datastore will cope” is not a plan.
| Posture | Use when | Obligation |
|---|---|---|
| Reset | All data is disposable and clearly labelled illustrative. | State the loss before changing shape. |
| Read old, write new | Few revisions coexist briefly. | Test both read paths and a retirement date. |
| Transform once | Records must survive as one consistent set. | Back up, count, transform and verify totals. |
For our invented project, 73 old records converting to 73 new records is a useful count, but not enough. Sample dates, durations, owners and decision reasons too. Arithmetic catches missing rows; assertions catch distorted rows.
Connect shape to behaviour
Walk each field through the transition table. Does every guard have the facts it needs? Walk it through the failure matrix. Can a rejected transition be explained without exposing internal values? Finally, put representative records into the test budget.
The six-pass framework treats this data pass as one lens, because records cannot be judged in isolation. A beautifully normalised shape that supports the wrong user outcome is still wrong.
Honest limitation
This single-record technique does not replace relational design, retention analysis, privacy assessment, concurrency control or capacity planning. Regulated, high-scale, safety-critical, medical, financial, real-time and security-critical systems require specialist review and stronger change controls.