
Introduction
This article is a follow-up to the time zone article. Read that article first before this one.
When building distributed Java applications with Spring Boot, Hibernate, and PostgreSQL, timezone handling can become a nightmare. Your application servers might be running in different time zones, but you want to store all timestamps in UTC without any automatic conversions. The last thing you want is Hibernate silently converting your carefully prepared UTC timestamps based on the server’s local timezone.
This article will show you how to configure Hibernate to store and retrieve dates exactly as-is, without any timezone conversions, and discuss the best Java temporal types for UTC storage.
The Solution: Configure Hibernate for UTC
Primary Configuration
The most crucial setting is telling Hibernate to use UTC for all JDBC operations. Add this to your application.properties:
spring:
jpa:
properties:
hibernate:
jdbc:
time_zone: UTC
Additional Safety Measures
For extra protection against timezone issues:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
// Set JVM timezone to UTC
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
SpringApplication.run(Application.class, args);
}
}
Configure your DataSource with timezone-aware properties:
@Configuration
public class DatabaseConfig {
@Bean
@ConfigurationProperties("spring.datasource")
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.addDataSourceProperty("serverTimezone", "UTC");
config.addDataSourceProperty("useTimezone", "true");
config.addDataSourceProperty("useLegacyDatetimeCode", "false");
return new HikariDataSource(config);
}
}
Alternatively or additionally depending on whether you will sometimes access using JDBC without the data source, you could add the following configuration.
spring.datasource.url: jdbc:postgresql://localhost:5432/yourdb?TimeZone=UTC
LocalDateTime vs Instant for UTC Storage
When storing UTC timestamps, you have two primary options: LocalDateTime and Instant. Each has distinct advantages and trade-offs.
LocalDateTime: The “What You See Is What You Get” Approach
LocalDateTime represents a date-time without timezone information. When you store a LocalDateTime, Hibernate saves it exactly as specified.
@Entity
public class Event {
@Column(name = "created_at")
private LocalDateTime createdAt;
// If you create it with UTC time
LocalDateTime utcTime = LocalDateTime.of(2023, 12, 25, 15, 30, 0);
// It gets stored exactly as: 2023-12-25 15:30:00
}
Pros of LocalDateTime:
- Simplicity: No timezone complexity – what you store is what you get
- Performance: No timezone calculations during persistence or retrieval
- Database Clarity: Maps cleanly to
TIMESTAMP WITHOUT TIME ZONEin PostgreSQL - Predictable: Behavior is consistent regardless of server timezone
- Human Readable: Easy to understand when examining database directly
Cons of LocalDateTime:
- Ambiguity: No inherent timezone information – requires discipline to ensure UTC
- Error Prone: Developers might accidentally store local time instead of UTC
- No Timezone Safety: Can’t validate that the stored time is actually UTC
- Conversion Responsibility: Application must handle UTC conversion before storage
Instant: The “Absolute Point in Time” Approach
Instant represents a specific moment in time (always UTC internally). It’s the most precise temporal type for UTC storage.
@Entity
public class Event {
@Column(name = "created_at")
private Instant createdAt;
// Always represents UTC
Instant now = Instant.now(); // Current UTC time
// Stored as timestamp, always in UTC
}
Pros of Instant:
- Timezone Safety: Always represents UTC – impossible to store wrong timezone
- Precision: Nanosecond precision for high-accuracy requirements
- Unambiguous: Clear semantic meaning – always an absolute point in time
- API Consistency: Works seamlessly with other
java.timeclasses - Future Proof: Best choice for distributed systems and microservices
Cons of Instant:
- Database Mapping: Can be less intuitive when examining database directly
- Conversion Overhead: May require conversion to
LocalDateTimefor display - Learning Curve: Developers need to understand
Instantsemantics - Hibernate Behavior: Some older Hibernate versions had quirks with
Instant
Practical Example: Entity Design
Here’s how you might design an entity with both approaches:
@Entity
@Table(name = "user_activities")
public class UserActivity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Option 1: LocalDateTime (ensure UTC before storing)
@Column(name = "created_at_local")
private LocalDateTime createdAtLocal;
// Option 2: Instant (always UTC)
@Column(name = "created_at_instant")
private Instant createdAtInstant;
// Usage example
public static UserActivity createNew() {
UserActivity activity = new UserActivity();
// LocalDateTime approach - must ensure UTC
activity.setCreatedAtLocal(LocalDateTime.now(ZoneId.of("UTC")));
// Instant approach - always UTC
activity.setCreatedAtInstant(Instant.now());
return activity;
}
}
Service Layer Best Practices
@Service
@Transactional
public class UserActivityService {
public UserActivity createActivity(String action) {
UserActivity activity = new UserActivity();
activity.setAction(action);
// If using LocalDateTime - be explicit about UTC
activity.setCreatedAt(LocalDateTime.now(Clock.systemUTC()));
// If using Instant - naturally UTC
// activity.setCreatedAt(Instant.now());
return userActivityRepository.save(activity);
}
// For display purposes, convert to user's timezone
public String getFormattedActivityTime(Long activityId, ZoneId userTimezone) {
UserActivity activity = userActivityRepository.findById(activityId).orElseThrow();
// Convert UTC LocalDateTime to user's timezone for display
ZonedDateTime userTime = activity.getCreatedAt()
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(userTimezone);
return userTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"));
}
}
Testing Your Configuration
@Test
@Transactional
public void testNoTimezoneConversion() {
// Test with LocalDateTime
LocalDateTime utcTime = LocalDateTime.of(2023, 12, 25, 15, 30, 0);
UserActivity activity = new UserActivity();
activity.setCreatedAt(utcTime);
userActivityRepository.save(activity);
UserActivity retrieved = userActivityRepository.findById(activity.getId()).get();
// Should be exactly the same - no conversion
assertEquals(utcTime, retrieved.getCreatedAt());
// Test with Instant
Instant now = Instant.parse("2023-12-25T15:30:00Z");
UserActivity activity2 = new UserActivity();
activity2.setCreatedAtInstant(now);
userActivityRepository.save(activity2);
UserActivity retrieved2 = userActivityRepository.findById(activity2.getId()).get();
// Should be exactly the same
assertEquals(now, retrieved2.getCreatedAtInstant());
}
Conclusion
Preventing Hibernate from performing unwanted timezone conversions is crucial for distributed applications. The key is configuring hibernate.jdbc.time_zone=UTC and choosing the right temporal types for your use case.
Choose LocalDateTime if you want simplicity and are confident about UTC discipline in your team. Choose Instant if you want maximum timezone safety and semantic clarity. Either way, with proper Hibernate configuration, your UTC timestamps will be stored and retrieved exactly as intended, regardless of your server’s timezone.
Remember: the goal is predictable, consistent behavior across all your application instances. With these configurations, you’ll achieve that goal while maintaining clean, maintainable code.
Leave a comment