AlgoMaster Logo

DateTimeFormatter

Medium Priority21 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

The java.time types like LocalDate, LocalDateTime, and ZonedDateTime are great in memory, but every time an order is saved to a database, written to a CSV, rendered on a receipt, or read from a payload, the value crosses a boundary and has to become text. DateTimeFormatter is the type that handles both directions: turning a temporal object into a string a human or machine can read, and parsing a string back into the right temporal type. This lesson covers the predefined ISO formatters, custom patterns, locale-aware rendering, thread-safety, common pitfalls, and the small but useful DateTimeFormatterBuilder.

Why DateTimeFormatter Exists

Inside a program, a LocalDate is just three numbers: year, month, and day. On an order receipt, that date has to become something a customer can read, like May 14, 2026 or 14/05/2026. In a database column or a CSV file, it has to become a string the consumer can parse back unambiguously, usually something like 2026-05-14. A date received from an API comes in as a string and needs to become a LocalDate.

DateTimeFormatter is the bridge. It knows how to walk a temporal object and produce text in a specific shape, and it knows how to read text in that same shape and rebuild the temporal object. It replaces the old SimpleDateFormat from java.util, and the replacement is not just cosmetic. The new class is immutable, thread-safe, more flexible with locales and zones, and tightly integrated with every java.time type.

The same DateTimeFormatter instance drives both directions. Build it once with a pattern (or pick a predefined one), and from then on it can render any compatible temporal type to text and parse any compatible text back into a temporal type.

Here is the smallest possible example, formatting and parsing an order date on a customer receipt.

Two calls, one formatter, both directions covered. The rest of the lesson is about choosing the right pattern, handling locales and time zones, and avoiding the handful of common mistakes.

Predefined Formatters

Before reaching for a custom pattern, check whether DateTimeFormatter already has one. The class ships with a set of constants for the common ISO and HTTP date formats. These are immutable singletons, so reusing them is free.

ConstantExample outputTypical use
ISO_LOCAL_DATE2026-05-14Database DATE columns, JSON without time
ISO_LOCAL_TIME09:15:30Time-of-day, no zone
ISO_LOCAL_DATE_TIME2026-05-14T09:15:30Timestamps without zone
ISO_OFFSET_DATE_TIME2026-05-14T09:15:30+05:30Timestamps with UTC offset
ISO_ZONED_DATE_TIME2026-05-14T09:15:30+05:30[Asia/Kolkata]Full zoned timestamp with named zone
ISO_INSTANT2026-05-14T03:45:30ZUTC instants, log timestamps, APIs
ISO_DATE2026-05-14 or 2026-05-14+05:30Date with optional offset
ISO_DATE_TIME2026-05-14T09:15:30 or with offset/zoneFlexible parser, accepts several shapes
BASIC_ISO_DATE20260514Compact date, no separators
RFC_1123_DATE_TIMEThu, 14 May 2026 09:15:30 GMTHTTP Date and Last-Modified headers

The naming is consistent: ISO_* follows ISO 8601, BASIC_ISO_DATE is ISO 8601's compact form, and RFC_1123_DATE_TIME follows the HTTP specification.

Here is a single program that renders one e-commerce order timestamp through several of these formatters side by side.

A few details. ISO_INSTANT always renders in UTC; the Z at the end stands for zero offset. ISO_OFFSET_DATE_TIME keeps the offset but drops the zone name, while ISO_ZONED_DATE_TIME keeps both. RFC_1123_DATE_TIME is the format HTTP servers use in headers like Last-Modified, so use it when setting or parsing those.

Predefined formatters are constants. The JVM creates each one once at class load and they are reusable forever. There is no benefit to caching them separately, and there is no penalty to using them in hot paths.

The default toString() of Instant, LocalDateTime, and friends matches their ISO formatter already, which is why orderInstant.toString() and orderInstant.format(DateTimeFormatter.ISO_INSTANT) produce the same text. ISO formats are the cheapest option when both ends of the pipe are under the application's control.

Custom Patterns with ofPattern

When none of the predefined formatters match, build one from a pattern string with DateTimeFormatter.ofPattern. The pattern is made up of letters that stand for fields, plus literal characters that pass through verbatim.

Here are the pattern letters used most often.

LetterFieldExamples
yyear-of-era26, 2026
uyear (proleptic, includes negatives)2026, -0044
Mmonth-of-year5, 05, May, Mar
dday-of-month4, 14
Dday-of-year134
Eday-of-week nameThu, Thursday
Hhour-of-day, 24 hour0, 9, 23
hhour-of-am-pm, 12 hour1, 9, 12
mminute-of-hour0, 15, 59
ssecond-of-minute30
Sfraction of second123, 123456
aam or pmAM, PM
ztime zone nameIST, Pacific Standard Time
Zzone offset, RFC 822+0530, -0800
Xzone offset, ISO 8601+05:30, Z
Olocalized zone offsetGMT+5:30
Vtime zone idAsia/Kolkata
xzone offset, no Z for zero+0530, +00

A few rules govern how the letters work.

  • The count of letters controls width. M is 5, MM is 05, MMM is May, MMMM is full word May (which happens to be three letters anyway, but for months like September you would see Sep vs September).
  • For numeric fields, the count is the minimum number of digits, zero-padded.
  • For text fields, three letters give short form, four give full form.
  • Unknown letters throw, so do not invent new ones. For a literal letter inside the pattern, wrap it in single quotes: 'T', 'at'.
  • A literal single quote inside a quoted block is written ''.

A practical e-commerce example: an order confirmation page wants to show the date as Thursday, May 14, 2026 at 9:15 AM.

The single-quoted 'at' is treated as a literal. Without the quotes, a would be interpreted as AM/PM and t would be an unknown letter and throw IllegalArgumentException at formatter construction.

The same pattern works in reverse for parsing. The example below reads a string from a CSV export and rebuilds the LocalDateTime.

Three pattern recipes that cover most everyday needs:

The yyyy-MM-dd recipe is the right default for storage; it sorts lexicographically the same way it sorts chronologically. The MMM dd, yyyy style is what most US receipts show. The yyyy-MM-dd'T'HH:mm:ssXXX recipe matches what ISO_OFFSET_DATE_TIME already produces; writing it out by hand only makes sense for small tweaks, like skipping the T or changing the offset format.

Pattern Pitfalls (yyyy vs YYYY, mm vs MM)

The pattern letters are case sensitive and a few pairs look nearly identical but mean very different things. These bugs ship to production because they only break on certain dates.

yyyy vs YYYY is the most common. Lowercase y is calendar year. Uppercase Y is week-based year, used with the ISO 8601 week numbering. They match for most dates, but at year boundaries they diverge.

December 29, 2025 falls in the first ISO week of 2026, so YYYY reports 2026 even though the calendar year is 2025. An order placed on this day would get stamped with a misleading year on the receipt. Always use lowercase yyyy for calendar year unless the use case specifically needs week-based year.

mm vs MM is the second classic. Lowercase m is minute, uppercase M is month.

The first output is nonsense: 2026-15-14 is not a date. It is the minute field rendered twice. The compiler cannot help here because both patterns are syntactically valid; only the runtime output reveals the mistake.

hh vs HH is the third one. Lowercase h is 12-hour clock and means nothing without a for AM/PM. Uppercase H is 24-hour clock.

The last two lines are the bug. hh:mm without a renders 9am and 9pm as the same string. An email reading "Your order ships at 09:00" without AM or PM produces customer confusion.

The last pitfall worth flagging is forgetting the zone when formatting a ZonedDateTime. If the pattern only mentions date and time letters but no z, Z, X, O, or V, the zone information just falls off the end of the string. Parsing the result back produces a LocalDateTime, not a ZonedDateTime. Always include a zone or offset token in patterns intended for zoned values that need to round-trip.

Formatting and Parsing

The two operations are mirror images and the API exposes both directions.

To format, call format on the temporal value and pass the formatter, or call format on the formatter and pass the temporal value. Both return a String and do exactly the same thing.

To parse, call the static parse(text, formatter) method on the target type. Each temporal type has its own version: LocalDate.parse, LocalDateTime.parse, ZonedDateTime.parse, OffsetDateTime.parse, and so on.

The target type has to match what the formatter can supply. Calling LocalDate.parse on a string that has time fields but no date fails. Calling LocalDateTime.parse on a string that has a zone drops the zone but the rest works; to preserve the zone, use ZonedDateTime.parse.

When parsing fails, the result is a DateTimeParseException, which is unchecked but worth catching when the input comes from outside the program.

The exception message includes the offending index, which is handy when debugging a CSV import that fails on row 4,217.

Parsing is heavier than formatting. The parser has to validate each field, resolve ambiguities, and check ranges. Parsing the same text shape thousands of times in a hot loop benefits from building the formatter once outside the loop; building it inside the loop allocates a fresh state machine every iteration.

A common e-commerce use of parsing is reading API payloads. A backend that receives an order timestamp as a string in the ISO_OFFSET_DATE_TIME format needs to convert it to local time for the admin dashboard.

The payload comes in as a UTC instant string, gets parsed into an OffsetDateTime, gets converted to the dashboard operator's local zone, and gets rendered with a custom pattern. Three formatter operations, two directions.

Locale-Aware Formatting

So far every example has used the JVM's default locale, which mostly means English. That is fine for internal logs and database storage. For anything a customer reads, the date should match their own language and conventions. May becomes Mai in German, Mayo in Spanish, मई in Hindi, and the order of fields shifts too.

There are two ways to bring the locale into the formatter.

Option 1: `withLocale` on a pattern formatter. This keeps the chosen layout and only translates the text fields (month names, day names, AM/PM).

Same dates, same layout, different language. withLocale returns a new formatter (the original is unchanged because DateTimeFormatter is immutable), so the base pattern can produce locale variants on demand.

Option 2: `ofLocalizedDate` / `ofLocalizedTime` / `ofLocalizedDateTime`. This lets the locale pick the layout too. Request a style (FULL, LONG, MEDIUM, SHORT) and the formatter picks the right shape for that region.

Each locale picks its own conventions. US writes 5/14/26, UK writes 14/05/2026, Germany writes 14.05.2026, Japan writes 2026/05/14. This is the right approach when the exact layout doesn't matter and the goal is for the customer to see something natural.

For an e-commerce app, ofLocalizedDate(FormatStyle.LONG) is a good default for product release dates, order confirmation pages, and shipping estimates. Pair it with the customer's locale (from their profile or the Accept-Language header) to skip a lot of formatting boilerplate.

One temporal value, four different rendered strings, depending on the locale handed to withLocale. The underlying LocalDate never changes.

withLocale is cheap because it returns a wrapper around the same pattern. Call it once per request and cache the result, or call it every time; the difference is not measurable for typical workloads.

Thread Safety and Reuse

Here is one of the most important practical facts about DateTimeFormatter. It is immutable and thread-safe. Declaring a static final formatter at the top of a class and sharing it across every thread costs only the single object created at class load.

This is a big change from the old SimpleDateFormat, which was mutable and not thread-safe. Many older codebases have a bug where the same SimpleDateFormat is used from multiple threads and starts returning garbage under load, because its internal Calendar state gets corrupted. The new API removes the bug class entirely.

That private static final pattern is the recommended production layout. Build the formatter once, use it everywhere. There is no synchronization cost, no per-call allocation, and no risk of state corruption.

The same is true for withLocale and withZone. These return new immutable instances without modifying the original, so chaining them is safe.

Same UTC instant, two pre-configured formatters that render it in different zones and languages. Both are static and shared. Both are safe to use from any thread.

SimpleDateFormat from java.util is not thread-safe. static final SimpleDateFormat in a legacy codebase is a latent bug. Replace it with DateTimeFormatter, which has the same expressive power and none of the concurrency hazards.

ResolverStyle: Strict, Smart, Lenient

When parsing, the formatter has to decide what to do with values that are out of range. For example, what should the parser do with 2026-02-30? February does not have 30 days. There are three answers, controlled by ResolverStyle.

StyleBehavior on 2026-02-30Default
STRICTThrows DateTimeParseException.Used by BASIC_ISO_DATE and some ISO formats.
SMARTAdjusts to the last valid day (2026-02-28).Default for ofPattern.
LENIENTOverflows into the next month (2026-03-02).Rarely appropriate.

The pattern uses uuuu for the year because STRICT requires a year field that allows era-free values. With yyyy the strict mode would also demand an era token.

When to override the default? Use STRICT for storage formats and APIs where bad input should fail loudly. Use SMART (the default) for user-typed input where forgiveness on edge cases like leap-day misses is appropriate. Use LENIENT almost never; it invents data without warning and is the source of subtle bugs.

For e-commerce purposes, treat incoming API timestamps as STRICT so import jobs fail loudly on bad data, and treat customer-entered dates from a form with the default SMART so a typo like "Feb 29 on a non-leap year" becomes "Feb 28" rather than a hard error.

DateTimeFormatterBuilder for Advanced Cases

ofPattern covers maybe 95 percent of formatting needs. The last 5 percent (optional sections, default values for missing fields, mixed locales, custom literals built up programmatically) is where DateTimeFormatterBuilder comes in. It assembles a formatter step by step.

The most common reason to use the builder is optional sections. A CSV import that sometimes has 2026-05-14T09:15:30 and sometimes just 2026-05-14 can use one pattern string with DateTimeFormatter.ofPattern("yyyy-MM-dd[ HH:mm:ss]"); the square brackets mark the optional section, and the builder powers that internally. The same thing can be built explicitly when more control is needed.

optionalStart and optionalEnd mark a section that the parser tries but does not require. The parseDefaulting calls supply fallback values for the time fields when the optional section is missing, so the result can still be a complete LocalDateTime even from a date-only input. This pattern fits CSV imports that have to swallow a few rows with missing time stamps without dropping them.

The builder can also compose formatters from existing ones, attach a default zone, or do case-insensitive parsing. For most code, ofPattern is enough; the builder covers the rest.

Pattern Letter Cheatsheet

For reference on letter meanings, this is the diagram to look at. Each box names the letter, what it represents, and a quick example for an order placed on May 14, 2026 at 9:15 AM IST.

The colors group the three families: date fields in cyan, time fields in orange, zone fields in teal. The repeat-count rules are consistent across families: more letters means wider zero padding for numeric fields, or longer text form for text fields.

Two quick mnemonics. The lowercase letters (y, m, d, h, s) are the fields written on a paper form. The uppercase ones (M, H, D) are the calendar-aware variants used in code.

Putting It Together: An Order Receipt and Email Subject

Here is a small program that uses everything in this lesson at once: a static thread-safe formatter, a locale-aware variant, a custom pattern for the email subject, and an ISO format for the database column.

Three formatters declared static final at the top of the class, reused across every call. One source ZonedDateTime rendered four different ways for four different audiences. No thread-safety worries, no per-call allocation, and the layouts can be tested in isolation by passing fixed inputs to each formatter.

This is the production shape: build formatters once, give them descriptive names, and reuse them.

Quiz

DateTimeFormatter Quiz

10 quizzes