Primitives That Do Not Drift

ForkTex Engineering · September 5, 2026 · 9 min

Every service we build touches three things before it does anything interesting: it mints an identifier, it records when something happened, and eventually it fails and has to say why. These are the least glamorous decisions in a system and the most expensive ones to revisit, because by the time they hurt, they are in every table, every log line and every client.

So we specified them once, put them in a shared library, and stopped having the conversation.

Identifiers are time-ordered, and never truncated

Every identifier we mint is a full UUIDv7.

Version 7 front-loads a timestamp, which buys two things. Primary-key inserts land at the right edge of the index instead of scattering into random leaves, so a busy table keeps appending rather than fragmenting. And rows sort by creation without a separate column to sort on.

The second rule matters more than the first: a UUIDv7 is never sliced.

This looks harmless:

trace_id = str(uuid7())[:8]

It is not a shortening. Because v7 front-loads the timestamp, the leading characters are almost entirely clock. Truncating to a prefix collapses the keyspace to roughly one distinct value per millisecond — so two requests in the same millisecond get the same "unique" identifier. That is a collision, dressed up as a convenience.

The pull toward it is real: a full UUID is ugly in a log line and worse in a support ticket. But a trace id exists so that a screenshot from a user becomes a log query, and a truncated one silently stops being able to do its job under exactly the load where you need it.

Identifiers are also minted as a column default rather than at the call site. Passing a freshly generated value into a constructor means the value exists before the ORM has decided whether it needs one, which quietly diverges from what the database would have done on its own.

Every timestamp is UTC, in one shape

One module decides how a moment becomes text and back. Not a convention, not a code-review habit — a module, which everything else calls.

It does very little:

  • now() returns a timezone-aware UTC datetime.
  • to_iso(value) converts to UTC and formats it.
  • from_iso(text) always returns a UTC-aware datetime.

The value is not in the functions, which are trivial. It is in there being exactly one of them. The failure mode this prevents is not a crash — it is two services each hand-rolling "the same" normalisation, agreeing for two years, then disagreeing about a daylight-saving boundary in a report nobody re-reads.

One honest wrinkle worth naming: naive input is assumed to be UTC rather than rejected. Strictly, that is wrong — a naive datetime carries no zone and guessing is how bugs start. It exists because callers predating the module relied on it, and breaking them all at once would have been worse than the concession. There is a strict mode that raises instead, and new code uses it. We would rather write that down than pretend the default is principled.

A related detail that bites people: precision follows the standard formatter, so microseconds appear only when they are non-zero. The string is not fixed-width. Anything parsing it by character offset will work until the first timestamp that lands exactly on a second.

An error carries a code, not a status

The third primitive is the one teams usually discover last.

An error is not an HTTP status. It becomes one at the edge, if the edge happens to be HTTP. The same failure raised inside a queue consumer, a scheduled job or a CLI has no status to carry and no transport to carry it on.

So our error type knows nothing about HTTP. It carries a machine-readable code, a human-readable message, optional structured details, and the trace id. Every service serialises failures into one envelope:

{
  "code": "validation",
  "message": "Start date must not be in the past.",
  "details": { "field": "startDate", "received": "2020-01-01" },
  "traceId": "019203f1-8c4a-7f3e-b1d2-5a6c8e0f1a2b"
}

One hierarchy, many transport mappings. An exception that knows it is a 404 cannot be raised from a worker without lying.

Two details earn their place. First, the vocabulary is closed in code but open on the wire: the shared codes are an enum, but the field serialises as a string, so a service can add its own domain codes without every other service learning about them. Second, driver text never reaches a client. Database error strings quote the offending values — which are frequently user data — name internal constraints and columns, and change between major versions. A client that pattern-matches on driver text breaks on upgrade, and it deserves to.

There is also a small defensive rule: serialising details falls back to a string representation rather than raising. An error-reporting path that itself throws turns a handled failure into an unhandled one and loses the original cause, which is the single most annoying way to lose an afternoon.

Why bother

None of this is clever. That is rather the point.

The cost of specifying these three things is a few hundred lines in a shared library and one argument, once. The cost of not specifying them is a slow accumulation of near-misses: an id format that differs between two services, a timestamp that is local time in one table, an error surface where half the routes return a code and half return prose.

None of those will take down a system on the day they are introduced. All of them make the system harder to reason about forever, and each one is nearly impossible to fix later, because by then it is load-bearing.