Learnitweb

Handling time zones in distributed systems

1. Introduction

Managing dates and times in applications is a frequent but complex challenge that demands careful design and implementation to ensure accuracy. This complexity increases when applications cater to users worldwide, each operating in different time zones. Additionally, many countries—or even specific regions within them—observe Daylight Saving Time (DST), adding another layer of intricacy that must be accounted for.

Some countries, including large ones like China and India, operate under a single official time zone despite their vast geographical size. In contrast, other countries manage multiple time zones within their borders to account for regional differences. The United States has 4 time zones in the contiguous states and 5 more in Alaska, Hawaii, and other US territories. Few countries has one or more separate daylight-saving policies.

Such challenges affect saving and searching for data with date filters, which can affect the functionality of applications.

This article examines the challenges involved and demonstrates a sample application implementation, complete with an API, to deliver a comprehensive solution for distributed environments. The provided sample code is compatible with Java 18 or newer versions.

2. Stating the problem

n the example business case presented in this article, your customers are located in various regions and may travel across different time zones while using your software. This requires anticipating that customers will create data in one time zone and access it from another, ensuring your application can handle these scenarios seamlessly. Consider a scenario where a customer creates data while in Egypt today, but tomorrow they travel to Morocco and access that same data. At the same time, a colleague in Australia might also access the data. In this case, your application needs to handle the time zone differences and ensure that the data is correctly displayed according to each user’s local time, regardless of where they are accessing it from.

The infrastructure may be spread across multiple time zones. One user and a server can be in the US time zone called Central Time. Similarly, a user in Sydney is in the same time zone as a server there. It is also possible that the users of the server are not in the same time zone.

Meanwhile, users in some time zones might need to access servers in different time zones.

3. Time zone configuration

When you install a service or application—particularly a JVM-based application, a database server, or an application server—it typically inherits the default time zone and date-time configuration from the underlying operating system. In the case of a mobile device, the time zone may be automatically updated by the operating system, especially when the device reconnects to a local network, such as after powering it on following a flight. This ensures the device stays synchronized with the correct local time zone.

Since the service is running on a distributed infrastructure, relying on the server’s default time zone is not feasible. Instead, you need to override the default time zone configuration, anticipating that customers may create data from various time zones.

The common solution in this case is to store date and time in a standardized format, typically Coordinated Universal Time (UTC), also known as Greenwich Mean Time (GMT), in both the database and logs. When a customer accesses date-related information, you then identify the user’s local time zone and dynamically convert the UTC timestamp into the appropriate local time zone.

Let us understand this with an example. A customer from Berlin created a data record, and his local time was 21:30. The record will be saved on the server as 20:30 after converting it to UTC. The customers from Tokyo and Sydney will access the same record and read the record creation time as 05:30 and 07:30 local time, respectively, of the following day.

4. Representing date and time information in Java

Java provides several ways to represent date and time information.

The java.sql Date and Timestamp classes. Java offers two classes, Date and Timestamp, in the JDBC API to represent date and time information separately. They reside in the package java.sql. It is recommended to not use these classes because they will tie your Java Persistence API (JPA) entities’ date and time representation to JDBC-related API classes.

The util.Date class. The single class java.util.Date represents both date and time information. In JPA entities, the date and time will be differentiated with a @Temporal type annotation such as the following:

@Temporal(TemporalType.DATE)
private Date date;
@Temporal(TemporalType.TIMESTAMP)
private Date time;

This is also not recommended because you need to use annotations to differentiate the date and time.

Java Date and Time API. With Java 8 in 2014, the java.time package was added to the platform as JSR 310.

The following are the advantages of using the java.time APIs:

  • The java.time classes are immutable and thread-safe.
  • They are ISO-centric especially with respect to the ISO 8601 date and time format, and they use consistent domain models for dates, time, durations, and periods. In addition, they have comprehensive utility methods for date and time operations.
  • The java.time classes provide comprehensive functionality to manipulate date and time attributes among multiple time zones.
  • Finally, java.time classes have been backported to Java 6 and Java 7.

The ZonedDateTime class. The java.time, ZonedDateTime class has a wide selection of interfaces and methods for time zone operations. ZonedDateTime is an immutable representation of a date-time with a time-zone. This class stores all date and time fields, to a precision of nanoseconds, and a time-zone, with a zone offset used to handle ambiguous local date-times. For example, the value “2nd October 2007 at 13:45.30.123456789 +02:00 in the Europe/Paris time-zone” can be stored in a ZonedDateTime. This class handles conversion from the local time-line of LocalDateTime to the instant time-line of Instant. The difference between the two time-lines is the offset from UTC/Greenwich, represented by a ZoneOffset.

5. Zoned date and time conversion in action

Following is an example of date and time conversion according to the zone.

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

public class MeetingApplication {

    public static void main(String[] args) {

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

        String meetingTime = "2022-07-10 10:30:00";

        ZoneId meetingZone = ZoneId.of("Europe/Belgrade");

        ZonedDateTime parsed = LocalDateTime.parse(meetingTime, formatter)
                .atZone(meetingZone);

        System.out.println("1. Meeting date and time with zone " + parsed);
        // will print: 2022-07-10T10:30+02:00[Europe/Belgrade]

        Instant instant = parsed.toInstant();

        // print or save (instant) in the database
        System.out.println("2. meeting time saved in database as UTC equivalent: " + instant);
        // will print: 2022-07-10T08:30:00Z

        // Initialize Duke time zone.
        ZoneId dukeZone = ZoneId.of("America/Los_Angeles");

        // The meeting time is retrieved from the database (instant) with Duke's time zone.
        ZonedDateTime dukeTime = ZonedDateTime.ofInstant(instant, dukeZone);

        System.out.println("3.1 Duke meeting will be at (formatted): " + dukeTime.format(formatter));
        // will print: 2022-07-10 01:30:00

        System.out.println("3.2 Duke meeting will be at: " + dukeTime);
        // will print: 2022-07-10T01:30-07:00[America/Los_Angeles]

        System.out.println("4. Again, check the meeting time: " + ZonedDateTime
                .ofInstant(instant, meetingZone)
                .format(formatter)); // will print: 2022-07-10 10:30:00
    }
}

Output

1. Meeting date and time with zone 2022-07-10T10:30+02:00[Europe/Belgrade]
2. meeting time saved in database as UTC equivalent: 2022-07-10T08:30:00Z
3.1 Duke meeting will be at (formatted): 2022-07-10 01:30:00
3.2 Duke meeting will be at: 2022-07-10T01:30-07:00[America/Los_Angeles]
4. Again, check the meeting time: 2022-07-10 10:30:00

This simple example gives you an idea about how to convert from one zoned date and time to another in a distributed system.

Consider that the Instant class is for computers. Date and time variant classes are for humans. The Instant class is the natural way of representing the time for computers, but it is often useless to humans. Instant is usually preferred for storage (for example, in a database), but you may need to use other classes such as ZonedDateTime when presenting data to a user. Thus the date and time are always saved in the database in the UTC format. The time zone should always be present for conversion from Instant to any date and time equivalent classes.

  • The database connection should always be set to Coordinated Universal Time (UTC). Fortunately, this is a configuration setting you make when creating the connection pool.
  • The time zone should always be present for conversion and for searching from Instant to any date and time equivalent classes.
  • JPA date and time fields could be Instant, LocalDate for only date storage, or ZonedDateTime as a time stamp equivalent.
  • In the API controller, the value object class could contain the date and time fields in the form of LocalDateTime or a String to be parsed later.

6. Automatically force database date and time conversions to UTC

You can use version 5.2 or later of Hibernate Object/Relational Mapping (ORM) to perform the automatic conversion to UTC by adding the following configuration.

If you are using Spring Boot, add this property to the application.properties yaml file:

spring.jpa.properties.hibernate.jdbc.time_zone = UTC

Based on this settings from persisting data, all date and time attributes will be converted into UTC by the framework itself. Using this setting will help remove redundant code needed for manual conversion between client-specific zoned date and time information to UTC when date and time fields are saved to the database. Instead, all such fields will automatically be converted to UTC by the framework before information is sent to the database.

7. Database design considerations

Suppose there are two columns START_ON and END_AT columns of type timestamp (called TIMESTAMP in H2), which holds all the date and time information plus time zone information. (In this application, the time zone will be UTC.). And if you need to save only the date part, for example, your customer’s birthday, the column should be of date type. By the way, when you design your database, please don’t define everything as a timestamp, because that adds complexity and wastes storage. Use timestamp only where it’s needed.

8. Domain-model design considerations

The most vital part to consider is that any database field of type timestamp is mapped to one of the following java.time package classes (ZonedDateTime, OffsetDateTime, and Instant). For example, the date type is mapped to java.time.LocalDate.

You need to determine in what time zone you are going to access this specific data (Europe/Sofia, although the data is created in a different time zone). You can use Swagger or use curl as follows:

curl -X 'GET' \
  'http://localhost:8090/api/v1/1?tz=Europe%2FSofia' \
  -H 'accept: */*'

9. Application layer design considerations

Following is the sample code to understand the record in Java:

public record TripRequest(
@NotBlank String timezone,
       @JsonProperty("start_on") @NotNull @FutureOrPresent LocalDateTime startOn,
       @JsonProperty("end_at") @NotNull @Future LocalDateTime endAt
       ) {
}

The record captures the customer dates in a variable of type LocalDateTime, alongside the customer time zone. Of course, in real-world applications, you would get the customer’s time zone from customer settings saved in the system, such as a user profile or device time-zone settings indicated by the user’s browser.

A crucial part is the mapping from LocalDateTime variables with a specified time zone to entity ZonedDateTime variables using my custom DateTimeUtil.parseDateTime() method.

public static ZonedDateTime parseDateTime(LocalDateTime timestamp, String timezone) {
        return ZonedDateTime.of(timestamp, ZoneId.of(timezone));
}

The response only needs to represent the dates that already exist in the database according to the customer’s time zone, so its type is String, as follows:

@JsonInclude(NON_NULL)
public record TripResponse(
          long id,
          @JsonProperty("record_timezone") String timezone,
          @JsonProperty("start_on") String startOn,
          @JsonProperty("end_at") String endAt,
          @JsonProperty("record_age") String recordAge
          ………) {
}\

The mapper will use the DateTimeUtil.toString(ZonedDateTime zonedDateTime, String timezone) method to convert and format the domain entity ZonedDateTime variables into the preferred customer time zone using the following implementation:

public static String toString(ZonedDateTime zonedDateTime, String timezone) {

        return zonedDateTime.toInstant()
                .atZone(ZoneId.of(timezone))
                .format(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN));
}

Finally, after the correct mapping, the final response is returned to the customer successfully.