Dates in Java have been a long weekend of migraines. We have all wrestled with Calendar, SimpleDateFormat, and time zones until the logs looked like static.
There is a way out, and it starts with Joda Time and a maturing effort that the platform calls JSR 310, the future home of a true java time story.
java util Date and Calendar are not kind to humans. Date carries both a point on the line and a bag of deprecated tricks, so half the time you are guessing what it represents. Calendar is mutable in odd ways, and the month is zero based which trips even careful folks. SimpleDateFormat is not thread safe, so a shared formatter in a servlet will bite you in production. Time zone math around daylight saving jumps is easy to get wrong and stack traces tell you nothing. You can make it work, but the amount of defensive code and unit tests says a lot about the pain level.
// Old school dance with Date and Calendar
TimeZone ny = TimeZone.getTimeZone("America/New_York");
Calendar cal = Calendar.getInstance(ny);
// Careful: month constants or you get May by writing 4
cal.set(2009, Calendar.MAY, 17, 0, 48, 37);
Date when = cal.getTime();
// DST around March or November can shift things in ways you do not expect
cal.add(Calendar.HOUR_OF_DAY, 1);
// SimpleDateFormat is not thread safe, do not share this instance
SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
String iso = fmt.format(when);Joda Time has been the grown up in the room for years. It gives you clear types like DateTime, LocalDate, and Instant with immutable behavior and predictable math. The defaults speak ISO right out of the box so toString gives you a clean timestamp without ceremony. DateTimeZone uses the same Olson database you already trust on your servers. You still need to think about gaps and overlaps around daylight saving switches, but the library makes those cases visible instead of silent footguns. The usual gripe is the bridge to JDBC and old APIs, where you convert back to Date or Timestamp and drag some risk along for the ride.
// Joda Time feels like the API we wanted all along
DateTimeZone ny = DateTimeZone.forID("America/New_York");
DateTime dt = new DateTime(2009, 5, 17, 0, 48, 37, 0, ny);
DateTime plusOneHour = dt.plusHours(1);
String iso = dt.toString(); // 2009-05-17T00:48:37-04:00
// Interop back to legacy when needed
java.util.Date legacy = dt.toDate();JSR 310 looks like the moment we finally grow up inside the JDK. The expert group is shaped by the same minds behind Joda Time, and the design reads like lessons learned the hard way. The goal is clear types for each idea you care about Instant for a point in time, LocalDate for a date without a zone, ZonedDateTime when you actually care about place. Everything is immutable and the parsing and formatting layer does not hide thread problems. There is even a Clock hook so tests can control time without hacks. The reference code lives under javax time right now and names may shift, but the shape is there and it feels right.
// Early JSR 310 vibe, names may change
import javax.time.ZonedDateTime;
import javax.time.ZoneId;
import javax.time.LocalDate;
import javax.time.Instant;
ZoneId ny = ZoneId.of("America/New_York");
ZonedDateTime zdt = ZonedDateTime.of(2009, 5, 17, 0, 48, 37, 0, ny);
ZonedDateTime next = zdt.plusHours(1);
String iso = zdt.toString(); // ISO by default
// When you only need a date without time or zone
LocalDate invoiceDate = LocalDate.of(2009, 5, 17);What should you do today Start by standardizing on Joda Time for new code and wrap your boundaries so old Date and Calendar are trapped at the edges. Pull time zone IDs from the same source across services and store timestamps as UTC Instant in the database with a separate field for the chosen display zone. Treat daylight saving transitions as first class cases and test them with clocks pinned to the changeover nights. Keep an eye on JSR 310 and try the early builds in a feature branch so you learn the idioms before they land. Your future self will thank you when the platform folds this work into the standard library and your code hardly moves.
Time stops being messy once we give it the types it deserves.