skip to content

You are setting the convention for how a multi-service system persists dates and times through JPA/Hibernate entities. How do you decide what each attribute stores — UTC instants, local wall-clock plus a zone, or plain calendar dates — and what do you standardise across the codebase?

level: principalimportance: nice to knowfreq 26%

answer

  1. Classify first: happened / calendar / future-local / duration
  2. Instant for events, LocalDate for dates
  3. Future local = wall clock + zone id, derive instant
  4. UTC in JVM, DB session, hibernate.jdbc.time_zone
  5. Assert raw column contents, not via the same mapping

basics

~20 s

Classify each attribute first: a past moment stores an Instant in UTC; a calendar fact stores LocalDate; a future local commitment stores wall-clock plus an explicit zone id so later zone-rule changes stay correct. Then standardise UTC everywhere in storage, conversion only at edges, and one mapping style per meaning.

solid answer

~60 s

I start from meaning, not from types: 1. **Something that happened** (`created_at`, `captured_at`) — an `Instant`, stored UTC. Comparable and orderable across services. 2. **A calendar fact** (birth date, invoice date, accounting period) — `LocalDate`. Never derive it from an instant, or rows near midnight land on the wrong day. 3. **A future local commitment** ("the meeting is at 09:00 Berlin time next March") — wall-clock (`LocalDateTime` or date + time) **plus an explicit zone id column**, and compute the instant on read. Freezing an instant at booking time is wrong if that zone's offset rules change before the date. 4. **A duration or interval** — store as an amount, not as two ambiguous timestamps. Then the standards: UTC for the JVM, the database session and `hibernate.jdbc.time_zone`; java.time only, never `java.util.Date`; explicit fractional-second precision on timestamp columns; zone conversion only at the API/UI boundary; column naming that encodes the kind (`_at` for instants, `_date` for calendar dates, `_local` + `_zone_id` for wall-clock pairs); and round-trip tests that assert raw column contents through plain SQL rather than through the same mapping.

code

java · 12 lines
java
@Entity
public class Appointment {
    @Id @GeneratedValue private Long id;

    private Instant createdAt;            // happened: UTC instant
    private LocalDate invoiceDate;        // calendar fact

    private LocalDateTime startsAtLocal;  // future local commitment ...
    private String startsAtZoneId;        // ... with its zone contract

    private Duration slotLength;          // an amount, not two timestamps
}

go deeper

for a junior

Focus on the simple rules: instants for events, LocalDate for dates, store UTC, convert for display.

for a middle

Add why LocalDateTime does not identify a moment, the three UTC settings that must agree, and the risk of truncation from default column precision.

for a senior

Cover the future-local-commitment case, offset-storage policy, migration hazards on existing data, and tests that assert raw column contents.

for a principal

Deliver it as an enforceable one-page convention spanning storage, wire format and events, name the costs (ad-hoc SQL friction, tzdata as an operational dependency, migration cost), and explain how the rules will be checked in review.

## The decision is about meaning, not types The common failure is choosing a type by shape ("it has a date and a time, so `LocalDateTime`") rather than by what the value means. A durable convention starts with a small taxonomy, then assigns a mapping to each category. **Category 1 — moments that already happened.** Audit columns, event times, payment captures. They are points on the timeline. Map to `Instant`, store UTC. Every service compares and orders them without negotiation, and rendering into a user's zone is a presentation concern. On PostgreSQL these land in `timestamptz`; on databases without a with-zone type they land in a plain timestamp bound in UTC. **Category 2 — calendar facts.** Birth dates, invoice dates, holidays, accounting periods. There is no instant involved: a birthday does not begin at a particular second. Map to `LocalDate`. The mistake to forbid explicitly is deriving these from instants — `instant.atZone(zone).toLocalDate()` shifts by a day for anyone whose zone differs from the one used at write time. **Category 3 — future local commitments.** A meeting, a shift, a scheduled maintenance window described as "09:00 in Berlin". If you freeze an `Instant` at booking time and the zone's offset rules change before the date — which governments do with months of notice — the event moves relative to the local clock, and the business meaning breaks. Store the wall-clock value **and** the zone id (`Europe/Berlin`), compute the instant when you need one, and re-derive after tzdata updates. Reserve this pattern for cases where the local clock is genuinely the contract. **Category 4 — durations and recurrence.** Store the amount (`Duration`, an integer of minutes, an RRULE string) rather than a pair of timestamps whose difference you hope is meaningful across a DST boundary. ## What I standardise - **UTC in three places, consistently:** the JVM (`TZ=UTC` in the container), the database session, and `hibernate.jdbc.time_zone=UTC`. Any one of them drifting reintroduces host-dependent behaviour, and the failure appears twice a year. - **java.time only in entities.** No `java.util.Date`, no `Calendar`, therefore no `@Temporal`. Immutable types remove defensive copying and make intent explicit. - **Explicit column precision.** Declare `TIMESTAMP(6)` (or the precision you need) rather than accepting a dialect default — MySQL `DATETIME` defaults to whole seconds and truncates silently. - **Naming that carries the category.** `*_at` for instants, `*_date` for `LocalDate`, `*_local` accompanied by `*_zone_id` for wall-clock pairs. A reviewer should be able to spot a category error in the column name alone. - **One offset-storage policy.** Either normalise to UTC everywhere, or use `@TimeZoneStorage(TimeZoneStorageType.COLUMN)` where the original offset is genuinely business data — decided once, not per entity. - **Boundary conversion.** Zones are applied in the API layer or UI, driven by the user's preference, never inside domain logic or repositories. - **Tests that can catch symmetric errors.** Write a known instant, then assert the raw column contents through plain JDBC/SQL. Asserting through the same mapping passes even when the mapping is wrong in both directions. ## Cross-service concerns In a multi-service system the storage decision leaks into the wire format. Standardise ISO-8601 with an explicit offset (`2026-03-14T08:00:00Z`) in every payload; ban naive local strings in inter-service contracts. Where a service publishes events, the event's occurrence time is an instant, full stop — consumers must not have to guess a zone. If a downstream needs local rendering, publish the zone id alongside the instant rather than pre-rendering. ## Costs and tradeoffs to name aloud - **UTC everywhere makes ad-hoc SQL less friendly** for support staff who think in local time. Provide views or a documented conversion, rather than compromising the storage rule. - **Wall-clock plus zone is more moving parts.** Restrict it to categories where it is genuinely required; if everything gets a zone column, the convention has failed. - **Migrating an existing system is a data migration**, not a config change: turning on UTC binding re-interprets historical rows. Plan a cutover, snapshot distributions, verify against external evidence. - **tzdata is a moving dependency.** If category 3 exists in the system, JVM and database tzdata updates become an operational routine with a re-derivation step, not a silent patch. ## How I would present the decision A one-page rule set: the four categories with the mapping for each, the three UTC settings, the naming scheme, the wire-format rule, and two worked examples (an audit column and a booking). Reviewable, enforceable in code review or a lint rule, and short enough that people actually follow it — which matters far more than the elegance of any individual mapping.

  • Why is freezing an Instant wrong for a meeting booked months ahead in a named time zone?
    Because the instant encodes today's offset rules for that zone, and those rules can change before the date arrives — governments alter daylight-saving policy with modest notice. If the offset changes, a frozen instant renders at a different local clock time than the one the participants agreed. Storing the wall-clock value plus the zone id keeps the agreed local time authoritative and lets the instant be re-derived after a tzdata update.
  • What is the strongest single test that a time-handling convention is actually implemented?
    A round-trip test that writes a known value through the entity mapping and then asserts the raw column contents with plain SQL or JDBC. Asserting through the same mapping hides symmetric errors — a wrong zone applied on both write and read cancels out and the test passes while the database holds wrong digits that every other consumer will misread.
  • How do you keep support and analytics happy if everything is stored in UTC?
    Provide the conversion rather than weakening the storage rule: database views or helper functions that render key timestamps in the business's primary zone, documented conventions in the analytics layer, and UI that always displays in the viewer's zone. The storage layer stays unambiguous, and the local-time convenience lives where it belongs — at the presentation edge.

saying these in an interview costs you the question

  • Choosing types by shape rather than by meaning
  • Deriving a LocalDate from an Instant and losing a day near midnight
  • Freezing instants for future local commitments in named zones
  • Setting hibernate.jdbc.time_zone but leaving the JVM or database session on another zone
  • Verifying time behaviour only through the same mapping that writes the data

context