Definition
- JVM timezone – set with
-Duser.timezone=UTC(preferred) orTimeZone.setDefault(TimeZone.getTimeZone("UTC"))once at startup. - Jackson timezone –
spring.jackson.time-zone=UTCmakes JSON serialization consistent. - JDBC/Hibernate timezone –
hibernate.jdbc.time_zone=UTCso timestamp values are not silently shifted. - Docker/OS timezone –
ENV TZ=UTCin Dockerfile sets the container's zone, inherited by the JVM. - Best practice – set all layers to UTC; convert to user's local zone only at the presentation edge.
- Caution –
TimeZone.setDefault()affects the entire JVM process (all threads); set once at startup, never at runtime.
It is like the National Bank of India deciding that every clock in every branch, every form, and every computer runs on one agreed time standard. You can set that standard in several places: the building clock (OS/Docker), the staff handbook (JVM), the way reports are printed (JSON serialization), and the central ledger system (database). If you only set one and forget the others, different parts drift and disagree, so the right move is to set them all to the same standard: UTC.
Understanding the Concepts
Why does the default time zone matter?
A Java application's "default time zone" is not a single switch. It is resolved from several layers, and inconsistency between them causes the classic "works on my machine, wrong in production" time bug.
package com.bank.config;
import java.util.TimeZone;
import java.util.logging.Logger;
public class TimeZoneProblem {
private static final Logger LOG = Logger.getLogger(
TimeZoneProblem.class.getName());
public static void demonstrate() {
// Developer's machine: IST (Asia/Kolkata, UTC+5:30)
// Production server: UTC
// Result: timestamps differ by 5.5 hours
LOG.info("JVM timezone: " + TimeZone.getDefault().getID());
LOG.info("Current time: " + new java.util.Date());
// On dev: 2024-06-15 14:30:00 IST
// On prod: 2024-06-15 09:00:00 UTC
// Same instant, different representation
}
}
How do you set the JVM time zone?
The preferred method is the system property -Duser.timezone=UTC. Alternatively, call TimeZone.setDefault() very early in startup. This affects every new Date(), legacy Calendar, and any code using the system default.
package com.bank.config;
import java.util.TimeZone;
import java.util.logging.Logger;
public class TimeZoneConfig {
private static final Logger LOG = Logger.getLogger(
TimeZoneConfig.class.getName());
/**
* Call this ONCE at application startup, before any date processing.
*/
public static void initializeUtc() {
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
LOG.info("JVM timezone set to: " + TimeZone.getDefault().getID());
}
}
# Preferred: JVM argument
java -Duser.timezone=UTC -jar bank-service.jar
# Dockerfile
ENV TZ=UTC
ENTRYPOINT ["java", "-Duser.timezone=UTC", "-jar", "bank-service.jar"]
How do you set the Jackson serialization time zone?
Spring Boot's spring.jackson.time-zone property tells Jackson which zone to use when serializing and deserializing date fields.
# application.properties
spring.jackson.time-zone=UTC
spring.jackson.date-format=yyyy-MM-dd'T'HH:mm:ss.SSSZ
package com.bank.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.util.TimeZone;
public class JacksonConfig {
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.setTimeZone(TimeZone.getTimeZone("UTC"));
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}
How do you set the database session time zone?
Align the JDBC or Hibernate time zone so the driver interprets timestamps consistently. Without this, writing a TIMESTAMP to the database can silently shift by the server's offset.
# Hibernate property
spring.jpa.properties.hibernate.jdbc.time_zone=UTC
# JDBC URL parameter (PostgreSQL)
spring.datasource.url=jdbc:postgresql://host:5432/bankdb?options=-c timezone=UTC
-- PostgreSQL: set session timezone
SET timezone = 'UTC';
SHOW timezone; -- UTC
-- Verify: a timestamp is stored and read in UTC
SELECT NOW() AT TIME ZONE 'UTC' AS utc_now,
NOW() AT TIME ZONE 'Asia/Kolkata' AS ist_now;
How do you set the Docker/OS time zone?
The Docker TZ environment variable sets the container's timezone. The JVM reads this at startup if -Duser.timezone is not set.
# Dockerfile
FROM eclipse-temurin:21-jre
ENV TZ=UTC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
COPY bank-service.jar /app/bank-service.jar
ENTRYPOINT ["java", "-Duser.timezone=UTC", "-jar", "/app/bank-service.jar"]
What happens when the layers disagree?
If the JVM is UTC but the database session is IST, timestamps are silently shifted. This is the source of most timezone bugs.
package com.bank.service;
import java.sql.*;
import java.time.*;
import java.util.logging.Logger;
public class TimeZoneMismatchDemo {
private static final Logger LOG = Logger.getLogger(
TimeZoneMismatchDemo.class.getName());
public void demonstrateMismatch(Connection conn) throws SQLException {
// JVM: UTC, Database session: IST (UTC+5:30)
// Writing: app sends 2024-06-15T09:00:00 UTC
// DB interprets as: 2024-06-15T09:00:00 IST = 2024-06-15T03:30:00 UTC
// Reading back: 2024-06-15T03:30:00 UTC
// Shifted by 5.5 hours!
LOG.severe("Timezone mismatch: JVM=UTC, DB=IST -> 5.5 hour shift");
LOG.info("Fix: set hibernate.jdbc.time_zone=UTC");
}
}
How should you handle user-facing time display?
Store everything in UTC. Convert to the user's local zone only at the presentation edge using explicit ZonedDateTime.
package com.bank.service;
import java.time.*;
import java.time.format.DateTimeFormatter;
public class TimeDisplayService {
private static final DateTimeFormatter DISPLAY_FORMAT =
DateTimeFormatter.ofPattern("dd-MMM-yyyy hh:mm a z");
/**
* Convert a UTC instant to the user's local zone for display.
*/
public String formatForUser(Instant utcInstant, String userZoneId) {
ZonedDateTime userTime = utcInstant.atZone(ZoneId.of(userZoneId));
return userTime.format(DISPLAY_FORMAT);
}
public static void main(String[] args) {
TimeDisplayService service = new TimeDisplayService();
Instant now = Instant.parse("2024-06-15T09:00:00Z");
// Rajesh in Delhi sees IST
System.out.println(service.formatForUser(now, "Asia/Kolkata"));
// 15-Jun-2024 02:30 PM IST
// Priya in London sees BST
System.out.println(service.formatForUser(now, "Europe/London"));
// 15-Jun-2024 10:00 AM BST
}
}
Output:
15-Jun-2024 02:30 PM IST
15-Jun-2024 10:00 AM BST
What are common mistakes?
The most common mistake is setting the timezone at only one layer and assuming the others follow. Another mistake is calling TimeZone.setDefault() at runtime, which affects all threads.
Common Mistakes: Mistake Fix ──────────────────────────────── ────────────────────────────── Set JVM only, forget DB session Set hibernate.jdbc.time_zone Set Docker TZ, forget JVM prop Always pass -Duser.timezone Call setDefault() at runtime Call once at startup only Use local zone in business logic Store UTC, convert at display Rely on host's implicit zone Set explicitly at every layer
What should a senior engineer know?
TimeZone.setDefault() affects the entire JVM process and every thread. Modern code should prefer explicit java.time zones over leaning on any default. The near-universal best practice is UTC at every layer.
Senior Checklist:
[ ] -Duser.timezone=UTC in JVM args
[ ] spring.jackson.time-zone=UTC in application.properties
[ ] hibernate.jdbc.time_zone=UTC in JPA properties
[ ] ENV TZ=UTC in Dockerfile
[ ] Database session timezone = UTC
[ ] Business logic uses Instant/ZonedDateTime, never Date()
[ ] User-facing display converts to local zone at the edge
Visual Overview
┌──────────────────────────────────────────────────────┐ │ Time Zone Layers in a Java Application │ ├──────────────────────────────────────────────────────┤ │ │ │ Layer 1: OS / Docker │ │ ┌──────────────────────────┐ │ │ │ ENV TZ=UTC │ Container timezone │ │ └────────────┬─────────────┘ │ │ │ inherited by │ │ ▼ │ │ Layer 2: JVM │ │ ┌──────────────────────────┐ │ │ │ -Duser.timezone=UTC │ Affects new Date(), │ │ │ TimeZone.setDefault() │ Calendar, legacy code │ │ └────────────┬─────────────┘ │ │ │ │ │ ▼ │ │ Layer 3: JSON Serialization │ │ ┌──────────────────────────┐ │ │ │ spring.jackson.time-zone │ API response dates │ │ │ = UTC │ │ │ └────────────┬─────────────┘ │ │ │ │ │ ▼ │ │ Layer 4: Database Session │ │ ┌──────────────────────────┐ │ │ │ hibernate.jdbc.time_zone │ Timestamp read/write │ │ │ = UTC │ │ │ └──────────────────────────┘ │ │ │ │ All layers must agree on UTC! │ └──────────────────────────────────────────────────────┘
Key Takeaway
Yes, you can and should set a default time zone for the whole application, but the important insight is that the "default" is resolved from several layers and they must agree. At the JVM level, set -Duser.timezone=UTC or call TimeZone.setDefault() once at startup. At the serialization layer, set spring.jackson.time-zone=UTC. At the database layer, set hibernate.jdbc.time_zone=UTC. At the container layer, set ENV TZ=UTC in the Dockerfile.
The crucial senior point is to set these consistently rather than piecemeal. The classic "works on my machine, wrong in production" time bug comes from the layers disagreeing. A service at the National Bank of India ran correctly in development where hosts were IST but produced timestamps shifted by 5.5 hours in a cloud region with a different host zone, precisely because it relied on the implicit host default. Pankaj fixed it by pinning UTC at all four layers and converting to IST only when rendering to users with explicit ZonedDateTime.
Modern code should prefer java.time types with explicit zones over leaning on any default. TimeZone.setDefault() affects the entire JVM process and every thread, so it must be set once at startup and never toggled at runtime. Store everything in UTC internally and convert to the user's local zone only at the presentation edge.
Remember one sentence: set a default time zone at the JVM, Jackson, database, and Docker layers and standardize all of them on UTC so the whole application behaves deterministically everywhere.
---