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

Timezone and Locale: Hidden Requirements in Global Apps

Posted on July 26, 2008 By Luis Fernandez
\n

�The sun never sets on your users, but your code still sleeps in one timezone.�

\n\n\n\n

Why timezone and locale sneak into every feature

\n\n\n\n

We are shipping software to browsers and phones from everywhere. The App Store just opened and folks in Tokyo, Madrid, and S�o Paulo are signing up the same day. That is great for growth and also a magnet for sneaky bugs. The two most silent troublemakers I keep running into are timezone and locale. These are not shiny features. They are the wiring behind everyday things like timestamps on receipts, the total on an invoice, or the date you show in an email subject. Get them wrong and you get chargebacks, angry tickets, and long nights.

\n\n\n\n

This is a hands on look at Java i18n from real projects. The code examples use Java 6, servlets, JSP, Spring, and friends that many of us run today. The lessons are the kind that survive framework fashion. If your app has users outside your hometown, this matters.

\n\n\n\n

The night a clock jump ate our jobs

\n\n\n\n

We had a daily billing job set for 02:15 server time. It ran fine until a spring forward in the south flipped the clock and that time never existed. No job ran, no emails went out, and we found out when support woke up to a queue full of confused customers. A week later the opposite happened. Clocks moved back, the 01:30 hour happened twice, and we double charged a tiny slice of users. Same code, two different failures. The culprit was not the job runner. It was us treating local wall time as if it were hard truth.

\n\n\n\n

In another incident, a merchant in Mexico saw totals with a comma for decimals and thought a ten dollar order was a thousand. Their browser locale was es MX, our formatter ignored it, and we printed 1,000 where they expected 1.000. The numbers were right in the database. The display broke trust. That one email thread taught me to never let locale be a footnote.

\n\n\n\n

Deep dive 1: Store and move time in UTC

\n\n\n\n

You can avoid most timezone bugs by treating UTC as the only truth for storage and transport. Convert to the user timezone at the edges. That rule looks simple because it is. The tricky part is doing it consistently across app servers, JDBC, and the database.

\n\n\n\n

Server default. Set JVM default timezone to UTC on every server. Do not rely on whatever the OS thinks.

\n\n\n\n
public class Bootstrap {\n  static {\n    java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("UTC"));\n  }\n}
\n\n\n\n

Persist with a UTC Calendar. With JDBC, always pass a UTC calendar when setting or reading timestamps. This avoids surprise conversions.

\n\n\n\n
TimeZone utcTZ = TimeZone.getTimeZone("UTC");\nCalendar utc = Calendar.getInstance(utcTZ);\n\n// write\nPreparedStatement ps = conn.prepareStatement(\n  "INSERT INTO events(id, occurred_at_utc) VALUES(?, ?)"\n);\nps.setLong(1, id);\nps.setTimestamp(2, new Timestamp(System.currentTimeMillis()), utc);\nps.executeUpdate();\n\n// read\nPreparedStatement q = conn.prepareStatement(\n  "SELECT occurred_at_utc FROM events WHERE id = ?"\n);\nq.setLong(1, id);\nResultSet rs = q.executeQuery();\nif (rs.next()) {\n  Timestamp ts = rs.getTimestamp(1, utc);\n  long epochMillis = ts.getTime();\n}
\n\n\n\n

Pick the right column type. In MySQL, DATETIME is naive and stores the string of wall time with no timezone. TIMESTAMP is stored in UTC and converted based on connection settings. Both can be fine if you are consistent. Either set the connection timezone to UTC, or stick to DATETIME and treat values as UTC by contract. In PostgreSQL, use TIMESTAMP WITH TIME ZONE to normalize to UTC.

\n\n\n\n

Use ISO 8601 for APIs and logs. Build and parse UTC strings like 2008-07-26T02:21:05Z. Avoid time abbreviations like PST. They are ambiguous across places and seasons.

\n\n\n\n
SimpleDateFormat iso = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");\niso.setTimeZone(TimeZone.getTimeZone("UTC"));\n\nString stamp = iso.format(new Date()); // "2008-07-26T02:21:05Z"\nDate parsed = iso.parse("2008-07-26T02:21:05Z");
\n\n\n\n

Do not use week year by accident. In Java patterns, Y is week based year. y is calendar year. Using Y can shift into next year around the last week of December. This shows up in reports and invoices. Use y.

\n\n\n\n
// Good\nnew SimpleDateFormat("yyyy-MM-dd")\n\n// Risky around year end\nnew SimpleDateFormat("YYYY-MM-dd")
\n\n\n\n

Consider Joda Time. The core Date and Calendar APIs are mutable and easy to misuse. Joda gives you an immutable DateTime and a better DateTimeZone model.

\n\n\n\n
org.joda.time.DateTime nowUtc = new org.joda.time.DateTime(\n  org.joda.time.DateTimeZone.UTC\n);\n\norg.joda.time.DateTime userTime = nowUtc.withZone(\n  org.joda.time.DateTimeZone.forID("America/Mexico_City")\n);
\n\n\n\n

Deep dive 2: Format and parse with Locale not guesswork

\n\n\n\n

Locale is not only language. It is how people write dates, numbers, and currency. Let the platform do the heavy lifting. Never hand roll separators with String.replace.

\n\n\n\n

Get the locale from the user. Best is a profile setting. Next best is the Accept Language header. As a fallback, use site default.

\n\n\n\n
// Servlet example\nLocale userLocale = request.getLocale(); // from Accept-Language\n// or prefer a stored user preference\n// Locale userLocale = user.getPreferredLocale();
\n\n\n\n

Format currency safely. Use NumberFormat with a specific Currency. Do not assume the default for the locale fits your product. Some stores charge in USD globally. Some need ARS or MXN. Choose it.

\n\n\n\n
Locale loc = new Locale("es", "AR");\nNumberFormat money = NumberFormat.getCurrencyInstance(loc);\nmoney.setCurrency(java.util.Currency.getInstance("ARS"));\n\nString total = money.format(new BigDecimal("1234.56"));\n// "ARS 1.234,56" for es-AR
\n\n\n\n

Locale sensitive dates. For display, build a DateFormat from the user locale. For URLs and storage, stick to ISO 8601 in UTC.

\n\n\n\n
Locale loc = Locale.FRANCE;\nDateFormat df = DateFormat.getDateInstance(DateFormat.LONG, loc);\nString shown = df.format(new Date()); // "26 juillet 2008" for fr-FR
\n\n\n\n

Messages with parameters. ResourceBundle plus MessageFormat lets translators move words and placeholders as needed. Hard coded string concat breaks in many languages.

\n\n\n\n
// messages_es.properties\nwelcome=Hola {0}, tu pedido #{1} llega el {2}\n\n// Java\nResourceBundle rb = ResourceBundle.getBundle("messages", new Locale("es"));\nString msg = MessageFormat.format(\n  rb.getString("welcome"),\n  user.getFirstName(),\n  order.getNumber(),\n  DateFormat.getDateInstance(DateFormat.MEDIUM, new Locale("es"))\n           .format(order.getEtaDate())\n);
\n\n\n\n

Use UTF 8 everywhere. From browser to database. Mismatch here shows as question marks and broken accents.

\n\n\n\n
// JSP top\n<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>\n\n// Servlet\nresponse.setCharacterEncoding("UTF-8");\nrequest.setCharacterEncoding("UTF-8");\n\n// MySQL JDBC URL\n// jdbc:mysql://host/db?useUnicode=true&characterEncoding=UTF-8
\n\n\n\n

Right to left text is rare in some apps but not optional if you sell worldwide. Plan for it in CSS by not fixing alignment unless the design demands it. Use logical properties where you can. For content that mixes scripts, insert Unicode bidi marks only when needed.

\n\n\n\n

Deep dive 3: DST, schedules, and the Olson database

\n\n\n\n

Daylight Saving Time is not just spring forward and fall back. Governments change rules, sometimes with short notice. Your stack learns about these changes from the Olson timezone database, which your OS and JVM bundle. If the rules change and your server does not have the update, your math is wrong for that place.

\n\n\n\n

Keep timezone data fresh. On Linux, update the tzdata package with your usual package manager. On Sun JVMs, run the tzupdater tool from Sun. This matters before new seasons in places like Brazil or Australia where rules are not stable.

\n\n\n\n

Pick zones by region. Use IANA region IDs like America/Buenos_Aires or Europe/Madrid. Do not use short names like CST, which refer to many places with different offsets or rules.

\n\n\n\n

Scheduling with wall time vs fixed cadence. Some jobs should track the clock on the wall at the user location. The 09:00 newsletter is a good example. Other jobs should run every N minutes from a UTC anchor, like a poller. Treat them differently.

\n\n\n\n
  • Wall time jobs: store timezone plus local time. Recompute the next fire time with the zone rules.
  • Fixed cadence jobs: store next fire as epoch millis in UTC and add your interval.
\n\n\n\n

Quartz with per zone triggers. Quartz lets you set a timezone on a CronTrigger. That keeps 09:00 in S�o Paulo consistent across DST changes.

\n\n\n\n
org.quartz.CronTrigger trigger = new org.quartz.impl.triggers.CronTrigger();\ntrigger.setCronExpression("0 0 9 * * ?");\ntrigger.setTimeZone(java.util.TimeZone.getTimeZone("America/Sao_Paulo"));\nscheduler.scheduleJob(jobDetail, trigger);
\n\n\n\n

Beware missing and repeated times. On the spring jump, times like 02:15 do not exist in some zones. On the fall repeat, 01:30 happens twice. For user input in a zone, validate and disambiguate.

\n\n\n\n
// Joda example disambiguation on fall repeat\nDateTimeZone zone = DateTimeZone.forID("America/New_York");\nLocalDate date = new LocalDate(2008, 11, 2);\nLocalTime time = new LocalTime(1, 30);\n\nList<Long> candidates = zone.getTransition(date.toDateTime(time, zone).getMillis()) != 0\n  ? java.util.Arrays.asList(\n      date.toDateTime(time, zone).getMillis(),\n      date.toDateTime(time, zone).plusHours(1).getMillis()\n    )\n  : java.util.Collections.singletonList(date.toDateTime(time, zone).getMillis());\n// present choices to user if ambiguous
\n\n\n\n

APIs that accept offsets only. Some external services only take a numeric offset like GMT minus 3. That is not enough around DST switches because the offset can change. If the API allows it, send region IDs. If not, refresh offsets often from a zone and cache them.

\n\n\n\n

Field checklist you can paste in a ticket

\n\n\n\n
  • Servers run with default timezone UTC. Logs print ISO 8601 with Z.
  • Database stores timestamps in UTC. JDBC uses a UTC Calendar for set and get.
  • User profile stores locale and preferred timezone. Do not guess.
  • All display formats use user locale. All parsing uses the same locale.
  • APIs and URLs carry time as ISO 8601 UTC strings.
  • Message bundles and MessageFormat handle text with parameters.
  • UTF 8 from request to response to database. No mixed encodings.
  • Quartz or cron jobs are labeled wall time or fixed cadence and scheduled accordingly.
  • OS tzdata and JVM tz rules updated before seasons change. Watch vendor notices.
  • No short timezone names. Only region IDs like America Mexico_City.
\n\n\n\n

Reflective close

\n\n\n\n

The past few weeks have been a rush. New users are finding apps from their phones and laptops in every corner. We spend hours on features that show up on screenshots, but the small choices about timezone and locale make the difference between smooth and brittle. The fixes are not mysterious. Store in UTC. Format with the user locale. Keep tzdata fresh. Be explicit about whether a schedule follows the sun or the clock in Greenwich.

\n\n\n\n

If you are starting a new module this weekend, wire these defaults first. If you have a mature codebase, audit the edges. Search for SimpleDateFormat without a timezone, for NumberFormat without a locale, and for raw string glue between numbers and labels. Swap in better patterns. Run tests in a different zone than your laptop. Flip the default locale for a test run. You will catch the same class of bugs that used to find us at 3 in the morning.

\n\n\n\n

Software now travels everywhere. The most reliable teams I know treat time and language as first class inputs just like user id or price. Do that and the sun can spin all it wants while your app keeps its cool.

\n
General Software 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