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

  1. 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.
  2. 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-DD strings. This is document the format, applied to every hop at once.
  3. 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.
  4. 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.


← Back to all topics