- Move cross-cutting docs (personas, design system, implementation phases, Ideen.md) to .specify/memory/ - Move cross-cutting research and plans to .specify/memory/research/ and .specify/memory/plans/ - Extract 5 setup tasks from spec/setup-tasks.md into individual specs/001-005/spec.md files with spec-kit template format - Extract 20 user stories from spec/userstories.md into individual specs/006-026/spec.md files with spec-kit template format - Relocate feature-specific research and plan docs into specs/[feature]/ - Add spec-kit constitution, templates, scripts, and slash commands - Slim down CLAUDE.md to Claude-Code-specific config, delegate principles to .specify/memory/constitution.md - Update ralph.sh with stream-json output and per-iteration logging - Delete old spec/ and docs/agents/ directories - Gitignore Ralph iteration JSONL logs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5.5 KiB
date, git_commit, branch, topic, tags, status
| date | git_commit | branch | topic | tags | status | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026-03-04T21:15:50+00:00 | b8421274b4 |
master | Date/Time Handling Best Practices for the fete Stack |
|
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
timestamptzovertimestamp— the PostgreSQL wiki says "don't usetimestamp".timestamptzmaps naturally toOffsetDateTime. - Hibernate 6 (Spring Boot 3.5.x) has native
OffsetDateTime↔timestamptzsupport.LocalDateTimerequires 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 bareLocalDateTimestring forces the client to guess the timezone. - The OpenAPI
date-timeformat andopenapi-generatordefault toOffsetDateTimein 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:
OffsetDateTimeserializes to"2026-03-15T20:00:00+01:00"(ISO 8601 string)LocalDateserializes to"2026-06-15"
No additional configuration needed. For explicitness, can add to application.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:
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
# 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 — recommends
timestamptzovertimestamp - PostgreSQL Docs: Date/Time Types
- Thorben Janssen: Hibernate 6 OffsetDateTime and ZonedDateTime
- Baeldung: OffsetDateTime Serialization With Jackson
- Baeldung: Map Date Types With OpenAPI Generator
- Baeldung: ZonedDateTime vs OffsetDateTime
- Reflectoring: Handling Timezones in Spring Boot
- openapi-typescript documentation