Skip to content
CMO & CTO
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

  • Digital Experience
    • Experience Strategy
    • Experience-Driven Commerce
    • Multi-Channel Experience
    • Personalization & Targeting
    • SEO & Performance
    • User Journey & Behavior
  • Marketing Technologies
    • Analytics & Measurement
    • Content Management Systems
    • Customer Data Platforms
    • Digital Asset Management
    • Marketing Automation
    • MarTech Stack & Strategy
    • Technology Buying & ROI
  • Software Engineering
    • Software Engineering
    • Software Architecture
    • General Software
    • Development Practices
    • Productivity & Workflow
    • Code
    • Engineering Management
    • Business of Software
    • Code
    • Digital Transformation
    • Systems Thinking
    • Technical Implementation
  • About
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

Dates and Times in Java: Why It Always Hurts

Posted on July 28, 2008 By Luis Fernandez

Dates and Times in Java: Why It Always Hurts

I shipped a promo scheduler last week. Midnight local time for each market, clean and simple. Except Buenos Aires woke up angry. The country flipped its daylight rules late in the season and our JVM still carried old tzdata. Our code used the server default zone, the promo fired an hour off, and support lit up. I had that familiar feeling you get when Java time bites. You stare at java.util.Date and Calendar, scroll through logs that show the right numbers and the wrong meaning, and realize the tools are old, leaky, and booby trapped. That morning I promised myself to write down the sharp edges before I forget where I bled. This is it.

First, the basics are not basic. Date is a millisecond instant with no time zone, but its toString() prints in the default zone, which confuses newcomers and tired seniors alike. Some Date constructors pretend to take year, month, day, and they are deprecated for good reason, yet they keep showing up in legacy code. Calendar is mutable, carries a zone, and packs more footguns than an action movie. Months start at zero. Weeks can start on Sunday or Monday depending on the locale. Arithmetic mutates internal fields unless you call getTime() at the right moment. Then comes SimpleDateFormat, where pattern letters look friendly until you mix up yyyy and YYYY and your new year reports land in the wrong bucket for the last week of December. None of this is new, but it keeps hurting because these classes sit at the center of everything.

The real trouble starts when you cross time zones and daylight changes. The only sane plan I have found is this: store instants in UTC as a long or a TIMESTAMP that is UTC, and carry the user zone as a separate string like America/Argentina/Buenos_Aires. Do not store offsets, they shift. Do not use short names like CET or PST, they are vague. When you need to show a date to a person, convert from UTC to their zone at the last moment. When you need to schedule a local clock time like midnight on the first of the month, build that local time in a calendar set to the user zone, then ask the calendar for the UTC instant. This forces you to face the ugly cases like spring forward gaps and fall back repeats. A local time of two thirty may not exist. Or it may exist twice. Your code has to pick a rule and do it consistently.

// Build midnight local and convert to UTC instant
TimeZone tz = TimeZone.getTimeZone("America/Argentina/Buenos_Aires");
Calendar local = Calendar.getInstance(tz);
local.clear(); // avoid junk from now
local.setLenient(false);
local.set(2008, Calendar.AUGUST, 1, 0, 0, 0); // months are zero based

// If midnight is skipped due to DST, roll forward to first valid time
if (!tz.inDaylightTime(local.getTime()) && !tz.useDaylightTime()) {
    // nothing special
}
// In practice you need to detect gaps. A simple trick:
Date candidate = local.getTime();
Calendar check = Calendar.getInstance(tz);
check.setTimeInMillis(candidate.getTime());
if (check.get(Calendar.HOUR_OF_DAY) != 0) {
    // We fell into a gap, advance minute by minute until valid
    do {
        local.add(Calendar.MINUTE, 1);
        candidate = local.getTime();
        check.setTime(candidate);
    } while (check.get(Calendar.MINUTE) != 0 || check.get(Calendar.HOUR_OF_DAY) != 0);
}

long utcMillis = local.getTimeInMillis(); // this is a UTC instant

Now for the silent killer in production. SimpleDateFormat is not thread safe. Put one in a static field, hit it with traffic, and your logs will glitch in ways that make you question your sanity. Sometimes the month ticks backward. Sometimes a space turns into a digit. The fix is boring and strict. Either create a new formatter each time, or use a ThreadLocal, or guard with a lock. The first is safe and slow but usually fine. The second is fast and tidy. The third works but invites contention and surprises. If you must parse incoming dates from third parties, reject anything without an explicit zone or offset, or document that you will treat it as UTC. Be picky with patterns. Use yyyy-MM-dd for dates, HH:mm:ss for time in twenty four hour clock, and add Z for offsets like +0000 when needed. If the partner sends month names, specify a fixed locale like Locale.US.

// Wrong: shared formatter
private static final SimpleDateFormat BAD = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// Safer: ThreadLocal per pattern
private static final ThreadLocal<SimpleDateFormat> SDF =
    new ThreadLocal<SimpleDateFormat>() {
        @Override protected SimpleDateFormat initialValue() {
            SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
            f.setLenient(false);
            f.setTimeZone(TimeZone.getTimeZone("UTC"));
            return f;
        }
    };

public static String formatUtc(Date d) {
    return SDF.get().format(d);
}

public static Date parseUtc(String s) throws ParseException {
    return SDF.get().parse(s);
}

Databases bring their own bag of gotchas. Store UTC as a number if you can. A BIGINT with epoch millis is dead simple. When you need an index, numbers are your friend. If you must use SQL temporal types, understand what your driver and server do with zones. MySQL DATETIME has no zone and keeps the literal fields. TIMESTAMP stores a UTC instant and converts in and out using the session zone, which is great when you control it and awful when you do not. I have seen apps that call setTimestamp with a calendar in UTC and then read it back without the calendar, and they swear the database ate their hours. The truth is boring. The driver did what it was told. Pick one contract and stick to it. For reporting, precompute the local date bucket in the target zone and store it next to the instant to avoid recalculating for every query.

// JDBC tip: pin to UTC at the edge
TimeZone utc = TimeZone.getTimeZone("UTC");
Calendar utcCal = Calendar.getInstance(utc);

PreparedStatement ps = conn.prepareStatement(
    "insert into events(id, occurred_at_ms, occurred_at_ts) values (?, ?, ?)");
ps.setLong(1, id);
ps.setLong(2, utcMillis);
ps.setTimestamp(3, new java.sql.Timestamp(utcMillis), utcCal);
ps.executeUpdate();

// For analytics, store a local bucket string alongside
String bucket = new SimpleDateFormat("yyyy-MM-dd", Locale.US) {{
    setTimeZone(TimeZone.getTimeZone("America/New_York"));
}}.format(new Date(utcMillis));
// insert bucket as a VARCHAR for quick group by

You can make life easier with better tools and habits. Joda Time gives you immutable types, clear names, and sane operations. DateTime for an instant with a zone. LocalDate for a date on a calendar without a zone. Period and Duration that do what they say. No hidden mutation. No off by one month. The change pays off the first time you need to add one month to January thirty one. You also want a single way to ask for the current time. Wrap System.currentTimeMillis() behind a service and inject it, so tests can freeze time. Write tests that straddle a daylight change in at least two zones. Keep an eye on tzdata updates and ship them with your app when your runtime lags behind. If your ops team sets the server clock by hand or drifts without NTP, you will be chasing ghosts all week.

// Joda Time examples
// Build a local midnight and get the UTC instant
org.joda.time.DateTimeZone zone = org.joda.time.DateTimeZone.forID("Europe/Berlin");
org.joda.time.LocalDate localDate = new org.joda.time.LocalDate(2008, 3, 30);
org.joda.time.LocalTime localMidnight = org.joda.time.LocalTime.MIDNIGHT;
org.joda.time.LocalDateTime ldt = new org.joda.time.LocalDateTime(
    localDate.getYear(), localDate.getMonthOfYear(), localDate.getDayOfMonth(), 0, 0);

// Handle missing midnight due to DST by letting Joda resolve forward
org.joda.time.DateTime dt = ldt.toDateTime(zone);
long utcInstant = dt.getMillis();

// Human readable and unambiguous
String iso = dt.toDateTime(org.joda.time.DateTimeZone.UTC).toString(); // 2008-03-29T23:00:00Z

// Freezing time for tests
interface Clock {
    long now();
}
class SystemClock implements Clock {
    public long now() { return System.currentTimeMillis(); }
}
class FakeClock implements Clock {
    private long t;
    FakeClock(long t) { this.t = t; }
    public long now() { return t; }
    public void tick(long delta) { t += delta; }
}

A few field notes that save hours. Always log both the UTC instant and the zone when you log a user facing time. Print offsets like +02:00 or a zone name to avoid guessing games later. Never do date math on formatted strings, parse back to a type or work in numbers. Beware of week based years. Use yyyy for calendar year unless you really want ISO week year with YYYY. Do not assume midnight exists. When you schedule jobs, decide whether you want wall clock time or a fixed period since the last run, then code that rule and write a test. For queues and retries, use monotonic numbers where you can. For anything that users see, use human calendars with zones. That split keeps both sides honest. And keep your JVM and tzdata updated. When a country flips the switch, your code better know tomorrow morning.


Dates and times in Java still sting because the defaults are strange and the world refuses to sit still. You can ship reliable time code if you accept a few truths. Instants live in UTC. Presentation lives in the user zone. Formatting is not thread safe. Calendars are tricky. Prefer numbers in storage, clear zones in messages, and better libraries in your code. Test around the ugly edges, keep your data fresh, and do not trust your eyes when a string looks right. The millisecond is the truth. Everything else is a story we tell humans. Make it a consistent one.

Software Architecture Software Engineering

Post navigation

Previous post
Next post
  • Digital Experience (94)
    • Experience Strategy (19)
    • Experience-Driven Commerce (5)
    • Multi-Channel Experience (9)
    • Personalization & Targeting (21)
    • SEO & Performance (10)
  • Marketing Technologies (92)
    • Analytics & Measurement (14)
    • Content Management Systems (45)
    • Customer Data Platforms (4)
    • Digital Asset Management (8)
    • Marketing Automation (6)
    • MarTech Stack & Strategy (10)
    • Technology Buying & ROI (3)
  • Software Engineering (310)
    • Business of Software (20)
    • Code (30)
    • Development Practices (52)
    • Digital Transformation (21)
    • Engineering Management (25)
    • General Software (82)
    • Productivity & Workflow (30)
    • Software Architecture (85)
    • Technical Implementation (23)
  • 2025 (12)
  • 2024 (8)
  • 2023 (18)
  • 2022 (13)
  • 2021 (3)
  • 2020 (8)
  • 2019 (8)
  • 2018 (23)
  • 2017 (17)
  • 2016 (40)
  • 2015 (37)
  • 2014 (25)
  • 2013 (28)
  • 2012 (24)
  • 2011 (30)
  • 2010 (42)
  • 2009 (25)
  • 2008 (13)
  • 2007 (33)
  • 2006 (26)

Ab Testing Adobe Adobe Analytics Adobe Target AEM agile-methodologies Analytics architecture-patterns CDP CMS coding-practices content-marketing Content Supply Chain Conversion Optimization Core Web Vitals customer-education Customer Data Platform Customer Experience Customer Journey DAM Data Layer Data Unification documentation DXP Individualization java Martech metrics mobile-development Mobile First Multichannel Omnichannel Personalization product-strategy project-management Responsive Design Search Engine Optimization Segmentation seo spring Targeting Tracking user-experience User Journey web-development

©2025 CMO & CTO | WordPress Theme by SuperbThemes