Time Zones Don’t Have to Be a Nightmare: Handling Dates Properly with Quarkus and Hibernate Panache
Store dates in UTC, convert like a pro, and never mess up a user’s local time again, with just a few lines of code.
If you've ever had a meeting scheduled in UTC, converted to PST, and displayed in your user's browser as JST, you know one thing: time zones are hard. But if you're building a Java backend with Quarkus, they don’t have to be.
In this tutorial, you'll build a Quarkus REST API that stores timestamps in UTC using Hibernate ORM with Panache and returns them in the user's preferred time zone. You’ll also learn best practices around date and time handling in Java so you never confuse midnight in Berlin with noon in New York again.
Let’s get coding.
Step 1: Scaffold Your Project
First, create a fresh Quarkus project using the Quarkus Maven Plugin. Open a terminal and run:
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=com.example \
-DprojectArtifactId=quarkus-timezone-tutorial \
-Dextensions="rest-jackson,hibernate-orm-panache,jdbc-postgresql"
cd quarkus-timezone-tutorial
This sets up a REST API project with:
Jackson for JSON serialization.
Panache ORM for simplified Hibernate.
PostgreSQL JDBC for persistence.
You’ll need JDK 11+, Maven 3.8+, and Podman (or if you can’t have fun, use Docker) to follow along.
And, as usual, you can find the example on my Github repository. Just go there, leave a star, and start from there.
Step 2: Define Your Entity
Let’s model an Event
with a timestamp. The Maven plugin scaffolded an MyEntity for you, let’s just rename that and add our own content:src/main/java/com/example/Event.java
package com.example;
import java.time.OffsetDateTime;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;
@Entity
public class Event extends PanacheEntity {
public String name;
public OffsetDateTime eventTimestamp;
}
Why OffsetDateTime
? Because it includes the UTC offset, making it perfect for storing consistent time-based values across multiple zones.
Step 3: Set Up PostgreSQL and Configuration
Quarkus Dev Services can do all the heavy lifting for your underneath. No need to spin up or configure a database. Just configure your application in src/main/resources/application.properties
:
quarkus.datasource.db-kind=postgresql
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
# Ensure JDBC stores all dates in UTC
quarkus.hibernate-orm.jdbc.timezone=UTC
This setup makes sure:
Hibernate uses UTC for all persisted timestamps.
The schema is re-created on every start, ideal for development.
Step 4: Build the REST API
Let’s expose basic CRUD endpoints for our events. Rename the GreetingResource.java and change to:src/main/java/com/example/EventResource.java
package com.example;
import java.util.List;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/events")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class EventResource {
@GET
public List<Event> listAll() {
return Event.listAll();
}
@POST
@Transactional
public Event create(Event event) {
event.persist();
return event;
}
}
This resource provides:
GET /events
: List all events.POST /events
: Create a new event with a name and timestamp.
Step 5: Test the API
Start your Quarkus application in development mode:
./mvnw quarkus:dev
Send a POST request to create an event in Central European Summer Time (UTC+2):
curl -X POST -H "Content-Type: application/json" -d '{
"name": "My Awesome Event",
"eventTimestamp": "2025-07-21T10:00:00+02:00"
}' http://localhost:8080/events | json_pp
You’ll get a response like this:
{
"eventTimestamp" : "2025-07-21T08:00:00Z",
"id" : 1,
"name" : "My Awesome Event"
}
The time has been automatically converted and stored in UTC.
Step 6: Convert to the User’s Time Zone
Storing in UTC is only half the battle. Now let’s give users the correct time in their own zone.
Add this method to EventResource.java
:
import jakarta.ws.rs.HeaderParam;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.stream.Collectors;
@GET
@Path("/local")
public List<Event> listAllInTimezone(@HeaderParam("X-Timezone") String timezone) {
List<Event> events = Event.listAll();
if (timezone == null || timezone.isEmpty()) {
return events; // Default to UTC
}
ZoneId zoneId = ZoneId.of(timezone);
return events.stream()
.peek(event -> {
ZonedDateTime zonedDateTime = event.eventTimestamp.atZoneSameInstant(zoneId);
event.eventTimestamp = zonedDateTime.toOffsetDateTime();
})
.collect(Collectors.toList());
}
Here’s what happens:
The user sends a
X-Timezone
header (e.g.,Europe/Paris
).The server converts the stored UTC timestamp to that zone before returning it.
Don’t forget to create an event first but you can than directly test it:
curl -H "X-Timezone: Europe/Paris" http://localhost:8080/events/local | json_pp
Result:
[
{
"eventTimestamp" : "2025-07-21T10:00:00+02:00",
"id" : 1,
"name" : "My Awesome Event"
}
]
Mission accomplished. You’ve created a timezone-aware API.
Recap and Best Practices
Time zones in enterprise applications can be a silent killer, causing confusion in scheduling, reports, and audits. Here’s how to avoid disaster:
Always store dates in UTC in your database.
Use
OffsetDateTime
(orZonedDateTime
) in Java to handle time zone data safely.Use headers like
X-Timezone
to let users control how they see dates.Set
quarkus.hibernate-orm.jdbc.timezone=UTC
in your config to enforce UTC at the JDBC level.
Where to Go Next
You can expand this example by:
Storing user preferences for time zones in a database.
Adding frontend formatting using JavaScript’s
Intl.DateTimeFormat
.Validating incoming time zones with a custom CDI validator.
You now have a production-ready strategy to make sure your app handles time correctly, no matter where your users are. Your logs, reports, and APIs will thank you.
Hi Marcus, is there a good reason, why you prefer OffsetDateTime over Instant?
Thank you for a nice article!