The lowest common denominator
A value that crosses N systems lives in the intersection of their type systems — design for the weakest hop, and make the loss explicit.
Your own client ↔ server assumes two ends and one
wire. Real values travel farther. A single timestamp might be produced by a
Python service, stored as a BSON Date in MongoDB,
serialized as RFC 3339 in a JSON response, parsed by
Swift on iOS and Kotlin on Android, and land in a warehouse and a spreadsheet
export before anyone looks at it. Every hop has its own idea of what a
timestamp is — and the value that survives the whole trip is whatever fits
the intersection of all of them. You do not get to design to your richest
type system; you inherit the poorest hop’s. The useful move is to notice this
early and choose the denominator deliberately, instead of letting the weakest
hop choose it for you mid-pipeline.
The denominators that shrink
Precision. java.time counts nanoseconds, Python’s datetime
microseconds, BSON Date and JavaScript’s Date milliseconds — and plenty
of wire formats and columns still carry whole seconds. The pipeline’s real
precision is the coarsest of these, no matter what the producer measured.
→ Precision
Zone awareness. A ZonedDateTime carries an IANA zone name; an RFC 3339
string carries only an offset; a BSON Date carries neither. The zone name
is usually the first casualty of serialization — and it is exactly the part
you need to reconstruct wall-clock meaning later.
→ Zones vs offsets
Civil types. PostgreSQL has a true date; BSON has an instant type but
nothing date-only; JSON has no date or time types at all —
everything temporal is a string plus a documented convention. A birthday that
leaves the database as anything but an explicit date string is one hop away
from becoming a fake midnight instant.
→ What is a date?
Range. Python’s datetime spans years 1–9999; JavaScript’s Date
roughly ±273,000 years around the epoch; a 32-bit epoch counter anywhere in
the chain caps the whole pipeline at January 2038. The narrowest range wins,
silently.
→ Unix time
The problems that arise
Round-trips stop being equal. A Python service writes a
microsecond-precision datetime to MongoDB; BSON truncates it to
milliseconds; the next read returns a value that is almost the one written.
Every equality-based mechanism downstream — change detection, dedup, sync
cursors, optimistic locking — now sees a difference that will never go away.
A sync job can re-upload the same records forever without anyone spotting
why.
Hops disagree about how to shorten. One layer truncates the extra digits (BSON), another rounds when formatting. Two representations of the same instant end up a millisecond apart, and ordering comparisons flip for values that straddle the boundary.
The zone vanishes in transit. An iOS client sends
2026-06-05T15:00:00+02:00; the backend correctly stores the instant; the
Android client renders it in the device’s zone. Every hop behaved correctly,
and the user still sees the wrong wall time for a venue-local event — because
no field ever carried Europe/Paris, and an offset cannot stand in for a
zone. The lowest common denominator of the
pipeline had no slot for zone identity, and nobody added one.
Pitfall: Comparing a freshly computed timestamp against one that has been through storage, at full producer precision. The stored copy was truncated by the weakest hop, so the comparison is false even when the values name the same event — and retry/sync logic built on that comparison runs hot forever. Clamp to the pipeline’s precision before storing and comparing.
The pattern
- Inventory the hops. Languages, databases, wire formats, queues, the warehouse, log pipelines, spreadsheet exports. The ones nobody mentions are the ones that truncate. Your schema is read by more systems than your application.
- Write the contract at the denominator. Decide, in writing, what the
pipeline carries — for example: instants are RFC 3339 UTC with
millisecond precision; zone identity rides in a sibling IANA-name field;
date-only values are
YYYY-MM-DDstrings. This is document the format, applied to every hop at once. - Clamp at the source, not mid-pipeline. Produce values already at contract precision, so every later hop round-trips losslessly and equality means what it says.
- Make remaining loss explicit. If a hop cannot carry something the contract needs (a zone name, sub-millisecond digits), carry it in a parallel field — or record the decision to drop it. Loss you chose is a trade-off; loss you discover is an incident.
See also: 3rd-party data for the hops you don’t control, Store vs display for the ends of the pipe, and Precision for the units side of the same story.
Go deeper: where the narrow ranges hide
Range limits rarely announce themselves, because each layer’s documentation
states its own range and none states the pipeline’s. Embedded devices and
older C libraries carry 32-bit time_t (overflowing 2038-01-19); MySQL’s
TIMESTAMP historically capped at 2038 while DATETIME does not; SQLite
stores whatever the application sends, so the effective range is the
narrowest reader. Far-future dates — contract end dates, “never expires”
sentinels like 9999-12-31 — are the values that find these cliffs. If your
pipeline must carry them, test the round-trip through every hop with exactly
those sentinels.