--- date: 2026-03-04T21:15:50+00:00 git_commit: b8421274b47c6d1778b83c6b0acb70fd82891e71 branch: master topic: "Date/Time Handling Best Practices for the fete Stack" tags: [research, datetime, java, postgresql, openapi, typescript] status: complete --- # Research: Date/Time Handling Best Practices ## Research Question What are the best practices for handling dates and times across the full fete stack (Java 25 / Spring Boot 3.5.x / PostgreSQL / OpenAPI 3.1 / Vue 3 / TypeScript)? ## Summary The project has two distinct date/time concepts: **event date/time** (when something happens) and **expiry date** (after which data is deleted). These map to different types at every layer. The recommendations align Java types, PostgreSQL column types, OpenAPI formats, and TypeScript representations into a consistent stack-wide approach. ## Detailed Findings ### Type Mapping Across the Stack | Concept | Java | PostgreSQL | OpenAPI | TypeScript | Example | |---------|------|------------|---------|------------|---------| | Event date/time | `OffsetDateTime` | `timestamptz` | `string`, `format: date-time` | `string` | `2026-03-15T20:00:00+01:00` | | Expiry date | `LocalDate` | `date` | `string`, `format: date` | `string` | `2026-06-15` | | Audit timestamps (createdAt, etc.) | `OffsetDateTime` | `timestamptz` | `string`, `format: date-time` | `string` | `2026-03-04T14:22:00Z` | ### Event Date/Time: `OffsetDateTime` + `timestamptz` **Why `OffsetDateTime`, not `LocalDateTime`:** - PostgreSQL best practice explicitly recommends `timestamptz` over `timestamp` — the PostgreSQL wiki says ["don't use `timestamp`"](https://wiki.postgresql.org/wiki/Don't_Do_This). `timestamptz` maps naturally to `OffsetDateTime`. - Hibernate 6 (Spring Boot 3.5.x) has native `OffsetDateTime` ↔ `timestamptz` support. `LocalDateTime` requires extra care to avoid silent timezone bugs at the JDBC driver level. - An ISO 8601 string with offset (`2026-03-15T20:00:00+01:00`) is unambiguous in the API. A bare `LocalDateTime` string forces the client to guess the timezone. - The OpenAPI `date-time` format and `openapi-generator` default to `OffsetDateTime` in Java — no custom type mappings needed. **Why not `ZonedDateTime`:** Carries IANA zone IDs (e.g. `Europe/Berlin`) which add complexity without value for this use case. Worse JDBC support than `OffsetDateTime`. **How PostgreSQL stores it:** `timestamptz` does **not** store the timezone. It converts input to UTC and stores UTC. On retrieval, it converts to the session's timezone setting. The offset is preserved in the Java `OffsetDateTime` via the JDBC driver. **Practical flow:** The frontend sends the offset based on the organizer's browser locale. The server stores UTC. Display-side conversion happens in the frontend. ### Expiry Date: `LocalDate` + `date` The expiry date is a calendar-date concept ("after which day should data be deleted"), not a point-in-time. A cleanup job runs periodically and deletes events where `expiryDate < today`. Sub-day precision adds no value and complicates the UX. ### Jackson Serialization (Spring Boot 3.5.x) Spring Boot 3.x auto-configures `jackson-datatype-jsr310` (JavaTimeModule) and disables `WRITE_DATES_AS_TIMESTAMPS` by default: - `OffsetDateTime` serializes to `"2026-03-15T20:00:00+01:00"` (ISO 8601 string) - `LocalDate` serializes to `"2026-06-15"` No additional configuration needed. For explicitness, can add to `application.properties`: ```properties spring.jackson.serialization.write-dates-as-timestamps=false ``` ### Hibernate 6 Configuration With Hibernate 6, `OffsetDateTime` maps to `timestamptz` using the `NATIVE` timezone storage strategy by default on PostgreSQL. Can be made explicit: ```properties spring.jpa.properties.hibernate.timezone.default_storage=NATIVE ``` This tells Hibernate to use the database's native `TIMESTAMP WITH TIME ZONE` type directly. ### OpenAPI Schema Definitions ```yaml # Event date/time eventDateTime: type: string format: date-time example: "2026-03-15T20:00:00+01:00" # Expiry date expiryDate: type: string format: date example: "2026-06-15" ``` **Code-generation mapping (defaults, no customization needed):** | OpenAPI format | Java type (openapi-generator) | TypeScript type (openapi-typescript) | |---------------|-------------------------------|--------------------------------------| | `date-time` | `java.time.OffsetDateTime` | `string` | | `date` | `java.time.LocalDate` | `string` | ### Frontend (TypeScript) `openapi-typescript` generates `string` for both `format: date-time` and `format: date`. This is correct — JSON has no native date type, so dates travel as strings. Parsing to `Date` objects happens explicitly at the application boundary when needed (e.g. for display formatting). ## Sources - [PostgreSQL Wiki: Don't Do This](https://wiki.postgresql.org/wiki/Don't_Do_This) — recommends `timestamptz` over `timestamp` - [PostgreSQL Docs: Date/Time Types](https://www.postgresql.org/docs/current/datatype-datetime.html) - [Thorben Janssen: Hibernate 6 OffsetDateTime and ZonedDateTime](https://thorben-janssen.com/hibernate-6-offsetdatetime-and-zoneddatetime/) - [Baeldung: OffsetDateTime Serialization With Jackson](https://www.baeldung.com/java-jackson-offsetdatetime) - [Baeldung: Map Date Types With OpenAPI Generator](https://www.baeldung.com/openapi-map-date-types) - [Baeldung: ZonedDateTime vs OffsetDateTime](https://www.baeldung.com/java-zoneddatetime-offsetdatetime) - [Reflectoring: Handling Timezones in Spring Boot](https://reflectoring.io/spring-timezones/) - [openapi-typescript documentation](https://openapi-ts.dev/)