---
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/)
