I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE COURSE

1. Overview

In this installment of the Reddit app case study, we’re going to be adding the be scheduling post’s according to the user’s timezone.

Dealing with timezones is notoriously difficult and the technical options are wide open. Our first concern is that we need to show dates to the user according to their own (configurable) timezone. We also need to decide what format the date will be saved as, in the database.

2. A New User Preference – timezone

First, we’ll add a new field – timezone – to our already existing preferences:

@Entity
public class Preference {
    ...
    private String timezone;
}

We then simply make the timezone configurable in the user Preferences Page – leveraging a simple but very useful JQuery plugin:

<select id="timezone" name="timezone"></select>
<script>
    $(function() {
        $('#timezone').timezones();
    });
</script>

Note that the default timezone is the server timezone – which runs on UTC.

3. The Controller

Now, for the fun part. We need to convert dates from the user’s timezone to the server’s timezone:

@Controller
@RequestMapping(value = "/api/scheduledPosts")
public class ScheduledPostRestController {
    private static final SimpleDateFormat dateFormat = 
      new SimpleDateFormat("yyyy-MM-dd HH:mm");
     
    @RequestMapping(method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.OK)
    public void schedule(
      @RequestBody Post post, 
      @RequestParam(value = "date") String date) throws ParseException 
    {
        post.setSubmissionDate(
          calculateSubmissionDate(date, getCurrentUser().getPreference().getTimezone()));
        ...
    }
     
    @RequestMapping(value = "/{id}", method = RequestMethod.PUT)
    @ResponseStatus(HttpStatus.OK)
    public void updatePost(
      @RequestBody Post post, 
      @RequestParam(value = "date") String date) throws ParseException 
    {
        post.setSubmissionDate(
          calculateSubmissionDate(date, getCurrentUser().getPreference().getTimezone()));
        ...
    }
    
    private synchronized Date calculateSubmissionDate(String dateString, String userTimeZone) 
      throws ParseException {
        dateFormat.setTimeZone(TimeZone.getTimeZone(userTimeZone));
        return dateFormat.parse(dateString);
    }
}

The conversion is pretty straightforward, but do note that it’s only happening on write operations – the server still returns UTC for reads.

That’s perfectly fine for our client, because we’ll do the conversion in JS – but it’s worth understanding that, for read operations, the server still returns UTC dates.

4. The Front-End

Now – let’s see how to use the user’s timezone in front-end:

4.1. Display the Posts

We will need to display the post’s submissionDate using the user’s timezone:

<table><thead><tr>
<th>Post title</th>
<th>Submission Date 
  (<span id="timezone" sec:authentication="principal.preference.timezone">UTC</span>)</th>
</tr></thead></table>

And here is our function loadPage():

function loadPage(page){
    ...
    $('.table').append('<tr><td>'+post.title+'</td><td>'+
      convertDate(post.submissionDate)+'</td></tr>');
    ...
}
function convertDate(date){
    var serverTimezone = [[${#dates.format(#calendars.createToday(), 'z')}]];
    var serverDate = moment.tz(date, serverTimezone);
    var clientDate = serverDate.clone().tz($("#timezone").html());
    var myformat = "YYYY-MM-DD HH:mm";
    return clientDate.format(myformat);
}

Moment.js helps here with the timezone conversion.

4.2. Schedule a new Post

We also need to modify our schedulePostForm.html:

Submission Date (<span sec:authentication="principal.preference.timezone">UTC</span>)
<input id="date" name="date" />

<script type="text/javascript">
function schedulePost(){
    var data = {};
    $('form').serializeArray().map(function(x){data[x.name] = x.value;});
    $.ajax({
        url: 'api/scheduledPosts?date='+$("#date").val(),
        data: JSON.stringify(data),
        type: 'POST',
        contentType:'application/json',
        success: function(result) {
            window.location.href="scheduledPosts";
        },
        error: function(error) {
            alert(error.responseText);
        }   
    }); 
}
</script>

Finally – we also need to modify our editPostForm.html to localize the submissonDate old value:

$(function() {
    var serverTimezone = [[${#dates.format(#calendars.createToday(), 'z')}]];
    var serverDate = moment.tz($("#date").val(), serverTimezone);
    var clientDate = serverDate.clone().tz($("#timezone").html());
    var myformat = "YYYY-MM-DD HH:mm";
    $("#date").val(clientDate.format(myformat));
});

5. Conclusion

In this simple article, we introduced a simple but highly useful feature into the Reddit app – the ability to see everything according to your own timezone.

This was one of the main pain points as I was using the app – the fact that everything was in UTC. Now – all dates are properly displayed in the timezone of the user, as they should be.

I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE LESSONS

newest oldest most voted
Notify of
Rafal Piotrowski
Guest
Rafal Piotrowski

You should not use SimpleDateFormat as static field in Controller class. SimpleDateFormat is not thread-safe. You can have format in static field and create new SimpleDateFormat(format) every time in it is needed. More sophisticated solution is to use ThreadLocal variable which holds SimpleDateFormat, but it not a simple solution, not suitable for tutorial about timezones 😉

Eugen Paraschiv
Guest

Hey Rafal, thanks for pointing that out. The method was supposed to be synchronized, but it wasn’t. In this case, that’s the easier fix, and because I’m not expecting a high throughout on these operations, I think that’s a good way to go initially. If I need anything else, I’ll definitely go either for multiple instances, or for a library. Cheers,
Eugen.

Rafal Piotrowski
Guest
Rafal Piotrowski

Synchronization is also a possible solution, but as you wrote it is good only when high throughout will not occur 🙂

Eugen Paraschiv
Guest

Agreed, if throughput ever becomes a goal for this app, I’ll remove the method level synchronization.

Mike
Guest
Mike

The simplest solution would be to create the format class each time you need it (which might even perform better under the heavy load anyway). Using synchronisation slows every request (although not dramatically). I would suggest fixing the code with this solution, as it’s mostly intended as an example.

However, you could have used the revised date-time classes from Java 8 (or even a precursor of it, a Joda-Time library) instead, which are thread safe by design.

Mike
Guest
Mike

Besides the usage of a shared non-thread safe class I see an issue with the architecture as well. Why is the server accepting and returning dates differently? That’s not only an accident waiting to happen but is also forcing you to convert timezones on both the UI and the server side. Having clear contract on the service to handle the dates in a consistent way, like accepting either only UTC or a user timezone, would make it clearer and easier to implement.

Eugen Paraschiv
Guest

The way the app handles timezones is – like most other things about the app – going to evolve; and in this first iteration, consistency of the API is in no way a goal. Long term – I agree, it would be better to have a consistent format both for in and out. In my view, this is how a case study is different from a standard article. A case study is all about pragmatic solutions and iterating in public. That’s why, once every few installments, we have an Improvements article – to pay down some of the technical debt… Read more »