Java DateTimeFormatter Class
Java DateTimeFormatter Class
DateTimeFormatter is the class that converts between java.time objects and their String representations, in both directions — formatting a LocalDate into readable text, or parsing text back into a LocalDate. It has already shown up throughout this series in pattern strings like "dd MMM yyyy", but it deserves a close look on its own, because most real date bugs in production — a wrong month showing up, a format that only breaks for users in a different country — trace back to a formatter pattern or a missing Locale, not to the date value itself.
What Is DateTimeFormatter?
java.time.format.DateTimeFormatter both formats an object into a String and parses a String back into an object. Unlike its predecessor SimpleDateFormat, it is immutable and thread-safe — a single instance can be created once, stored as a constant, and reused freely across every thread in an application with no risk of the corrupted output SimpleDateFormat was infamous for.
There are three main ways to get one: ofPattern() for a custom pattern string, one of the predefined ISO constants like ISO_LOCAL_DATE for standard formats, or ofLocalizedDate() and ofLocalizedDateTime() combined with a FormatStyle for locale-appropriate formatting without hardcoding a pattern at all.
One sentence before the diagram: the same DateTimeFormatter instance works in both directions, turning an object into text and text back into an object.
LocalDate.of(2026, 8, 25)
|
format(formatter)
v
"25 August 2026" (a String)
|
LocalDate.parse(text, formatter)
v
LocalDate.of(2026, 8, 25) <-- back to the original value
DateTimeFormatter is immutable, so the exact same instance used above for format() can be reused for parse(), and shared as a static final constant across every thread with no risk of the corrupted output a shared SimpleDateFormat could produce.
Why DateTimeFormatter Was Introduced
SimpleDateFormat's biggest liability was not its awkward syntax — it was that a single shared instance was unsafe to use from more than one thread at a time, a fact documented directly in its own Javadoc, and yet a natural pattern to reach for, since creating a fresh formatter per call felt wasteful.
1// File: BeforeDateTimeFormatter.java
2import java.text.*;
3import java.util.*;
4
5public class BeforeDateTimeFormatter {
6 // A shared SimpleDateFormat like this is a well-known source of
7 // corrupted output when accessed from multiple threads at once,
8 // since SimpleDateFormat is explicitly documented as not thread-safe
9 private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("dd-MM-yyyy");
10
11 public static void main(String[] args) throws ParseException {
12 Date orderDate = FORMATTER.parse("25-08-2026");
13 System.out.println(FORMATTER.format(orderDate));
14 }
15}Output:
25-08-2026
DateTimeFormatter produces identical output in single-threaded code, but the static field holding it is genuinely safe to share, because immutability removes the shared mutable state that made SimpleDateFormat dangerous in the first place.
1// File: AfterDateTimeFormatter.java
2import java.time.*;
3import java.time.format.*;
4
5public class AfterDateTimeFormatter {
6 // Safe to share as a static constant across every thread in the
7 // application, since DateTimeFormatter is immutable
8 private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy");
9
10 public static void main(String[] args) {
11 LocalDate orderDate = LocalDate.parse("25-08-2026", FORMATTER);
12 System.out.println(FORMATTER.format(orderDate));
13 }
14}Output:
25-08-2026
Both versions print the same date. Only the second one is actually safe to declare as a static final field the way both examples do — the first one just happens to work here because nothing else is touching it concurrently.
Syntax
Custom patterns, predefined ISO constants, and locale-aware styles cover nearly everything a real application needs.
1// File: DateTimeFormatterSyntaxForms.java
2import java.time.*;
3import java.time.format.*;
4import java.util.Locale;
5
6public class DateTimeFormatterSyntaxForms {
7 public static void main(String[] args) {
8 LocalDateTime moment = LocalDateTime.of(2026, 8, 25, 14, 30, 15);
9
10 DateTimeFormatter custom = DateTimeFormatter.ofPattern("EEEE, dd MMMM yyyy hh:mm a", Locale.US);
11 System.out.println("Custom pattern: " + moment.format(custom));
12
13 System.out.println("ISO_LOCAL_DATE_TIME: " + moment.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
14
15 DateTimeFormatter longStyle = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(Locale.US);
16 System.out.println("ofLocalizedDate(LONG): " + moment.toLocalDate().format(longStyle));
17
18 DateTimeFormatter shortStyle = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(Locale.US);
19 System.out.println("ofLocalizedDate(SHORT): " + moment.toLocalDate().format(shortStyle));
20 }
21}Output:
Custom pattern: Tuesday, 25 August 2026 02:30 PM
ISO_LOCAL_DATE_TIME: 2026-08-25T14:30:15
ofLocalizedDate(LONG): August 25, 2026
ofLocalizedDate(SHORT): 8/25/26
Common Use Cases
Parsing User Input With a Known Pattern
Supplying an explicit DateTimeFormatter to LocalDate.parse() tells the parser exactly what shape the incoming text is in, rather than relying on a default format the input might not match.
1// File: ParsingUserInputExample.java
2import java.time.*;
3import java.time.format.*;
4
5public class ParsingUserInputExample {
6 public static void main(String[] args) {
7 String rawInput = "25/08/2026";
8
9 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
10 LocalDate parsedDate = LocalDate.parse(rawInput, formatter);
11
12 System.out.println("Parsed: " + parsedDate);
13 }
14}Output:
Parsed: 2026-08-25
Formatting the Same Value for Two Different Audiences
A compact, machine-friendly format and a friendly, human-readable one can both be produced from the same underlying value with two separate formatters.
1// File: TwoAudienceFormattingExample.java
2import java.time.*;
3import java.time.format.*;
4import java.util.Locale;
5
6public class TwoAudienceFormattingExample {
7 public static void main(String[] args) {
8 LocalDateTime eventTime = LocalDateTime.of(2026, 8, 25, 14, 30, 0);
9
10 DateTimeFormatter logFormat = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss");
11 DateTimeFormatter displayFormat = DateTimeFormatter.ofPattern("dd MMM yyyy 'at' hh:mm a", Locale.US);
12
13 System.out.println("Log format: " + eventTime.format(logFormat));
14 System.out.println("Display format: " + eventTime.format(displayFormat));
15 }
16}Output:
Log format: 20260825T143000
Display format: 25 Aug 2026 at 02:30 PM
Producing Different Output From the Same Pattern by Locale
The exact same pattern string produces genuinely different text depending on the Locale it is combined with, which is precisely what makes hardcoding month or day names into a pattern risky.
1// File: LocaleSensitiveFormattingExample.java
2import java.time.*;
3import java.time.format.*;
4import java.util.Locale;
5
6public class LocaleSensitiveFormattingExample {
7 public static void main(String[] args) {
8 LocalDate date = LocalDate.of(2026, 8, 25);
9
10 DateTimeFormatter englishFormat = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US);
11 DateTimeFormatter frenchFormat = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.FRANCE);
12
13 System.out.println("Same pattern, English locale: " + date.format(englishFormat));
14 System.out.println("Same pattern, French locale: " + date.format(frenchFormat));
15 }
16}Output:
Same pattern, English locale: 25 August 2026
Same pattern, French locale: 25 août 2026
Including the Time Zone in a Formatted String
The VV pattern letter prints the actual zone id, which only a ZonedDateTime carries — LocalDate, LocalTime, and LocalDateTime have no zone for this pattern letter to draw from.
1// File: ZoneIdPatternExample.java
2import java.time.*;
3import java.time.format.*;
4import java.util.Locale;
5
6public class ZoneIdPatternExample {
7 public static void main(String[] args) {
8 ZonedDateTime meeting = ZonedDateTime.of(2026, 8, 25, 20, 0, 0, 0, ZoneId.of("Asia/Kolkata"));
9
10 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm VV", Locale.US);
11
12 System.out.println(meeting.format(formatter));
13 }
14}Output:
25 Aug 2026 20:00 Asia/Kolkata
Real-World Example
An order confirmation screen needs to show the same timestamp two different ways — compact for an order summary list, fully spelled out for a detailed receipt view — and the same formatting method needs to work correctly for any customer's locale without maintaining a separate hardcoded pattern per region.
1// File: Order.java
2import java.time.*;
3
4public record Order(String orderId, LocalDateTime placedAt) {}1// File: OrderConfirmationFormatter.java
2import java.time.*;
3import java.time.format.*;
4import java.util.*;
5
6public class OrderConfirmationFormatter {
7
8 public String formatForLocale(Order order, Locale customerLocale, FormatStyle style) {
9 DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(style).withLocale(customerLocale);
10 return order.orderId() + " placed on " + order.placedAt().format(formatter);
11 }
12}1// File: OrderConfirmationDemo.java
2import java.time.*;
3import java.time.format.*;
4import java.util.*;
5
6public class OrderConfirmationDemo {
7 public static void main(String[] args) {
8 Order order = new Order("ORD-1042", LocalDateTime.of(2026, 8, 25, 14, 30, 0));
9
10 OrderConfirmationFormatter formatter = new OrderConfirmationFormatter();
11
12 System.out.println(formatter.formatForLocale(order, Locale.US, FormatStyle.SHORT));
13 System.out.println(formatter.formatForLocale(order, Locale.US, FormatStyle.MEDIUM));
14 }
15}Output:
ORD-1042 placed on 8/25/26, 2:30 PM
ORD-1042 placed on Aug 25, 2026, 2:30:00 PM
formatForLocale never mentions a hardcoded pattern anywhere — passing a different FormatStyle changes the level of detail, and passing a different Locale would change the language and date ordering entirely, all through the same one method. A mistake that appears often in fresher pull requests is hardcoding a date pattern like "MM/dd/yyyy" directly into a formatting method, which quietly assumes every customer reads dates the American way. Accepting a Locale and a FormatStyle instead, and letting DateTimeFormatter choose the actual pattern, is what makes the same method correct for every customer without maintaining a different pattern string per region.
Combining DateTimeFormatter With Other Features
DateTimeFormatter is what every article in this Date and Time API series has relied on for display — LocalDate, LocalTime, LocalDateTime, and ZonedDateTime all accept one through their format() method and their static parse() method. The pattern letters used with ofPattern() correspond to fields defined on ChronoField, the same underlying temporal-field system Period and Duration draw on when computing a between() result. DateTimeFormatterBuilder exists for genuinely advanced cases — optional sections, case-insensitive parsing, custom resolver behavior — that ofPattern() alone cannot express, though most applications never need to reach for it directly.
Best Practices
Store every DateTimeFormatter as a static final constant and reuse it, exactly as the thread-safety example in this article demonstrates. Constructing a new one per call wastes work for no benefit, since the class was specifically designed to be built once and shared.
Always pass an explicit Locale to ofPattern() whenever the pattern includes a text-based field like a month or day name — MMMM, EEEE. Omitting it silently ties the output to whatever locale the running machine happens to be configured with.
Prefer ofLocalizedDate() or ofLocalizedDateTime() with a FormatStyle over a hand-written pattern whenever the actual goal is "format this the way this locale expects," rather than trying to maintain a different hardcoded pattern per region.
Remember that FormatStyle.LONG and FormatStyle.FULL for date-time formatting require an actual time zone to be present. Use them with ZonedDateTime, not a bare LocalDateTime, which has no zone for those styles to draw from.
Common Mistakes
Confusing MM with mm is the single most common DateTimeFormatter pattern mistake — uppercase MM means month, lowercase mm means minute, and swapping them compiles fine and produces a plausible-looking but completely wrong date.
1// File: MonthMinuteCaseMistake.java
2import java.time.*;
3import java.time.format.*;
4
5public class MonthMinuteCaseMistake {
6 public static void main(String[] args) {
7 LocalDateTime moment = LocalDateTime.of(2026, 8, 25, 14, 45, 0);
8
9 // WRONG - lowercase mm means minute, not month - this pattern
10 // accidentally prints the minute value where a month was intended
11 DateTimeFormatter wrongCase = DateTimeFormatter.ofPattern("dd-mm-yyyy");
12 System.out.println("dd-mm-yyyy (wrong): " + moment.format(wrongCase));
13
14 // CORRECT - uppercase MM means month
15 DateTimeFormatter correctCase = DateTimeFormatter.ofPattern("dd-MM-yyyy");
16 System.out.println("dd-MM-yyyy (correct): " + moment.format(correctCase));
17 }
18}Output:
dd-mm-yyyy (wrong): 25-45-2026
dd-MM-yyyy (correct): 25-08-2026
Using hh, the 12-hour pattern letter, without the accompanying a for AM/PM leaves the output genuinely ambiguous — two times exactly 12 hours apart print identically.
1// File: TwelveHourWithoutMarkerMistake.java
2import java.time.*;
3import java.time.format.*;
4import java.util.Locale;
5
6public class TwelveHourWithoutMarkerMistake {
7 public static void main(String[] args) {
8 LocalDateTime morningMeeting = LocalDateTime.of(2026, 8, 25, 9, 0, 0);
9 LocalDateTime eveningMeeting = LocalDateTime.of(2026, 8, 25, 21, 0, 0);
10
11 // WRONG - hh is 12-hour format, but without 'a' there is no way to
12 // tell 9 AM from 9 PM in the printed output
13 DateTimeFormatter missingMarker = DateTimeFormatter.ofPattern("hh:mm");
14 System.out.println("Morning (hh:mm only): " + morningMeeting.format(missingMarker));
15 System.out.println("Evening (hh:mm only): " + eveningMeeting.format(missingMarker));
16
17 // CORRECT - include 'a' whenever hh is used
18 DateTimeFormatter withMarker = DateTimeFormatter.ofPattern("hh:mm a", Locale.US);
19 System.out.println("Morning (hh:mm a): " + morningMeeting.format(withMarker));
20 System.out.println("Evening (hh:mm a): " + eveningMeeting.format(withMarker));
21 }
22}Output:
Morning (hh:mm only): 09:00
Evening (hh:mm only): 09:00
Morning (hh:mm a): 09:00 AM
Evening (hh:mm a): 09:00 PM
A 9 AM meeting and a 9 PM meeting print as the exact same string, 09:00, with the flawed pattern — a bug that is invisible until someone actually compares a morning event against an evening one.
Omitting Locale from a pattern containing month or day names ties the output to whatever locale the running machine happens to be configured with, which works fine in local testing and becomes unpredictable the moment the same code runs somewhere else.
1// File: MissingLocaleMistake.java
2import java.time.*;
3import java.time.format.*;
4import java.util.Locale;
5
6public class MissingLocaleMistake {
7 public static void main(String[] args) {
8 LocalDate date = LocalDate.of(2026, 8, 25);
9
10 // Omitting Locale ties the output to whatever the JVM's default
11 // locale happens to be - reliable in local testing, unpredictable
12 // the moment this code runs on a server configured differently
13 DateTimeFormatter noLocale = DateTimeFormatter.ofPattern("dd MMMM yyyy");
14
15 // Passing an explicit Locale makes the output the same everywhere
16 // this code runs, regardless of the machine's own configuration
17 DateTimeFormatter explicitLocale = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US);
18
19 System.out.println("With explicit Locale: " + date.format(explicitLocale));
20 System.out.println("Explicit Locale never depends on the running machine's configuration");
21 }
22}Output:
With explicit Locale: 25 August 2026
Explicit Locale never depends on the running machine's configuration
Interview Questions
Q1. What is DateTimeFormatter, and how is it different from SimpleDateFormat?
DateTimeFormatter both formats java.time objects into strings and parses strings back into them, and it is immutable and thread-safe, so a single instance can be shared freely as a constant. SimpleDateFormat performs the same conceptual job for java.util.Date, but it is explicitly documented as not thread-safe, making a shared static instance a well-known source of corrupted output under concurrent access. Interviewers ask this specifically to check whether a candidate knows the difference is about more than just syntax.
Q2. What is the difference between MM and mm in a formatting pattern?
Uppercase MM represents the month, and lowercase mm represents the minute. Mixing them up is one of the most common DateTimeFormatter bugs, since the pattern compiles and produces plausible-looking output that is simply wrong — a minute value like 45 showing up where a month was intended.
Q3. Why does a pattern using hh without the letter a produce ambiguous output?
hh formats the hour on a 12-hour clock, running from 01 to 12, with no indication of whether it is morning or afternoon. Without the a pattern letter appending AM or PM, two times exactly 12 hours apart — 9 AM and 9 PM — format to the identical string, making the output genuinely impossible to interpret correctly on its own.
Q4. What happens if you call ofPattern() with a pattern containing month or day names but no Locale?
The formatter uses the JVM's default locale to decide which language and convention to render those names in, which produces correct-looking output during local development and becomes unpredictable the moment the same code runs on a server configured with a different default locale. Passing an explicit Locale removes that dependency entirely.
Q5. What is the difference between ofPattern() and ofLocalizedDate()/ofLocalizedDateTime()?
ofPattern() uses an exact, hand-written pattern string that produces the same layout everywhere, regardless of locale, except for the actual text of locale-sensitive fields like month names. ofLocalizedDate() and ofLocalizedDateTime(), combined with a FormatStyle, let the locale itself decide the entire layout — field order, separators, and all — which is the correct choice whenever the goal is matching what a given locale's users actually expect to see, rather than enforcing one fixed format everywhere.
Q6. Why do FormatStyle.LONG and FormatStyle.FULL fail when formatting a plain LocalDateTime?
Both styles are documented to require an actual time zone, since a long or fully-detailed date-time display traditionally includes zone information. LocalDateTime has no time zone attached at all, so attempting to format one with ofLocalizedDateTime(FormatStyle.LONG) fails at runtime — ZonedDateTime is the type these two styles are actually designed to work with.
FAQs
Is DateTimeFormatter thread-safe?
Yes. DateTimeFormatter is immutable, so a single instance can be shared freely across threads with no synchronization required, which is exactly the property SimpleDateFormat was missing.
Can the same DateTimeFormatter be used for both formatting and parsing?
Yes. The same instance works in both directions — temporal.format(formatter) produces a String, and LocalDate.parse(text, formatter) or the equivalent method on other java.time types consumes one, both using the exact same pattern definition.
What does the pattern letter E or EEEE represent?
Both represent the day of the week, differing only in how many letters are used to render it — fewer letters produce an abbreviated form like Tue, while more letters, such as EEEE, produce the full name, Tuesday.
What is the difference between DateTimeFormatter.ISO_LOCAL_DATE and DateTimeFormatter.BASIC_ISO_DATE?
ISO_LOCAL_DATE produces the standard hyphenated ISO-8601 format, such as 2026-08-25. BASIC_ISO_DATE produces the same date with no separators at all, such as 20260825, which shows up in some file naming conventions and legacy system integrations.
Can DateTimeFormatter parse a date without knowing the exact pattern in advance?
Not reliably. DateTimeFormatter needs a defined pattern or predefined format to parse against — it cannot guess an arbitrary, unknown input format the way some looser parsing libraries attempt to. The input format needs to be known, or at least constrained to a small set of expected formats tried in sequence.
What exception is thrown if the input string doesn't match the formatter's pattern?
DateTimeParseException, an unchecked exception thrown by parse() when the input text does not conform to the pattern the formatter was built with.
Can I combine a custom literal string with pattern letters in the same ofPattern() call?
Yes, by wrapping the literal text in single quotes, exactly as "yyyyMMdd'T'HHmmss" and "dd MMM yyyy 'at' hh:mm a" do in this article's examples. Anything inside single quotes is printed exactly as written, rather than being interpreted as pattern letters.
Summary
DateTimeFormatter handles both directions of the boundary between a java.time object and readable text, and its defining advantage over SimpleDateFormat is not convenience — it is that a single, immutable instance is genuinely safe to declare once and share everywhere, instead of being a quiet source of concurrency bugs. ofPattern() covers custom formats, the ISO constants cover standard machine-readable ones, and ofLocalizedDate()/ofLocalizedDateTime() with a FormatStyle cover the case where the locale itself should decide the layout.
The habits worth carrying forward are checking pattern letter case carefully — MM versus mm has burned nearly every Java developer at least once — always including a alongside hh, and never leaving Locale unspecified on a pattern with text-based fields. Every one of those three mistakes compiles cleanly and only reveals itself once the wrong output actually reaches a user, which is exactly why they are worth internalizing now rather than debugging later.
What to Read Next
See why the old Date class was replaced by the new API.