UK PRE-LAUNCH PUBLICATION · ILLUSTRATIVE AUTHOR ROLE · TECHNICAL REVIEW NOT YET COMPLETED

Annotated-record note

Plan the Record Before Writing Behaviour

A record is a collection of promises. Make identity, absence, ownership and change explicit while erasing the draft still costs nothing.

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.

Three migration postures
PostureUse whenObligation
ResetAll data is disposable and clearly labelled illustrative.State the loss before changing shape.
Read old, write newFew revisions coexist briefly.Test both read paths and a retirement date.
Transform onceRecords 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.