Java Date & Time โ LocalDate, ZonedDateTime, Formatting & Best Practices
A complete guide to Java Date & Time โ from the broken legacy API to the modern java.time package. Covers LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Duration, Period, DateTimeFormatter, TemporalAdjusters, DST pitfalls, and production-ready patterns.
Last Updated
March 2026
Read Time
28 min
Level
Intermediate
Chapter
22 of 35
A Brief History of Java Date APIs โ Three Generations
Java's date and time handling has a famously troubled history โ spanning three distinct generations over 25+ years. Understanding why the API evolved the way it did is essential context for every Java developer: it explains why the codebase you maintain might be using three different date APIs simultaneously, and it makes the design decisions of java.time immediately clear.
The original java.util.Date was designed in haste and is widely regarded as one of Java's worst design decisions. It is mutable (not thread-safe), months are 0-indexed (January = 0), years are stored as (year - 1900), and despite being named 'Date' it also stores time. Most of its methods were deprecated in Java 1.1. Still found in legacy codebases and some JDBC APIs.
Calendar was introduced to fix Date's problems. It partially succeeded โ it added time zone support and fixed the year storage issue โ but it remained mutable, was even more verbose, still used 0-indexed months, and mixed date/time concepts in one bloated class. Calendar arithmetic was notoriously error-prone. The community turned to Joda-Time, a third-party library, as the de-facto standard.
java.time was designed by the author of Joda-Time (Stephen Colebourne) as JSR-310 and is a ground-up redesign. Key principles: immutability (all types are thread-safe), clear separation of concepts (date-only, time-only, with-zone, machine-time), fluent API, nanosecond precision, and comprehensive time zone support via the IANA timezone database. This is the only API you should use in new code written after Java 8.
Legacy API: java.util.Date & Calendar โ The Problems You Must Know
Even if you never write new legacy Date code, you will encounter it in production codebases, third-party libraries, and JDBC results. Understanding its specific failure modes prevents subtle bugs during maintenance โ and explains every interview question about why java.util.Date was replaced.
import java.util.Date;
import java.util.Calendar;
public class LegacyDateProblems {
public static void main(String[] args) {
// โ PROBLEM 1: Mutability โ not thread-safe
Date date = new Date();
date.setTime(0); // Can be mutated anywhere โ race conditions in multi-threaded code
// โ PROBLEM 2: Zero-indexed months (January = 0, December = 11)
// The MOST common java.util.Date bug ever:
Calendar cal = Calendar.getInstance();
cal.set(2026, 2, 20); // This is MARCH 20 โ not February! Month 2 = March
cal.set(2026, Calendar.MARCH, 20); // Correct way โ use constants
// โ PROBLEM 3: Year stored as (year - 1900)
Date d = new Date(2026 - 1900, 2, 20); // Deprecated constructor
System.out.println(d.getYear()); // Prints 126 โ not 2026!
System.out.println(d.getMonth()); // Prints 2 โ March, not 2!
// โ PROBLEM 4: Date also stores time (misleading name)
Date today = new Date(); // Has hours, minutes, seconds, millis too
// 'Date' is really a 'DateAndTime' โ milliseconds since Unix epoch
// โ PROBLEM 5: No time zone in java.util.Date
Date now = new Date(); // Always UTC internally, no zone stored
// Timezone only applied at formatting (SimpleDateFormat) level
// โ PROBLEM 6: SimpleDateFormat is NOT thread-safe
// DO NOT store SimpleDateFormat as a static field โ race condition!
// java.text.SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
// If two threads call sdf.format() simultaneously โ CORRUPT OUTPUT
// โ PROBLEM 7: Date comparison is error-prone
Date d1 = new Date();
Date d2 = new Date();
// d1 == d2 compares REFERENCES, not values
// d1.equals(d2) compares millisecond timestamps
// What about 'same day regardless of time'? No built-in way.
}
}java.time Package โ Architecture & Core Types
The java.time package (introduced in Java 8 as JSR-310) organises date/time concepts into a clear taxonomy. Every type is immutable and thread-safe. Every modification method (like plusDays() or withMonth()) returns a new instance โ the original is never modified. The API is fluent โ methods return the same type, enabling clean method chaining.
LocalDate โ Date Without Time or Zone
LocalDate represents a date โ year, month, day โ with no time component and no time zone. It is the right choice for any domain concept that is inherently date-only: a person's birthday, a public holiday, a project deadline, a payment due date. Internally, LocalDate stores year (int), month (Month enum), and day (int). It is immutable and thread-safe.
import java.time.LocalDate;
import java.time.Month;
import java.time.DayOfWeek;
import java.time.format.DateTimeFormatter;
public class LocalDateDemo {
public static void main(String[] args) {
// โ
Creating LocalDate
LocalDate today = LocalDate.now(); // Current date
LocalDate specific = LocalDate.of(2026, 8, 15); // Aug 15, 2026
LocalDate withEnum = LocalDate.of(2026, Month.AUGUST, 15); // Same, using enum
LocalDate parsed = LocalDate.parse("2026-08-15"); // ISO-8601 default
System.out.println(today); // e.g., 2026-03-20
System.out.println(specific); // 2026-08-15
// โ
Extracting fields
System.out.println(specific.getYear()); // 2026
System.out.println(specific.getMonth()); // AUGUST
System.out.println(specific.getMonthValue()); // 8 โ 1-indexed! (not 0!)
System.out.println(specific.getDayOfMonth()); // 15
System.out.println(specific.getDayOfWeek()); // SATURDAY
System.out.println(specific.getDayOfYear()); // 227
System.out.println(specific.lengthOfMonth()); // 31
System.out.println(specific.lengthOfYear()); // 365
System.out.println(specific.isLeapYear()); // false
// โ
Date arithmetic โ returns NEW instance (immutable)
LocalDate nextWeek = today.plusWeeks(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDate twoYearsLater = today.plusYears(2);
LocalDate firstOfMonth = today.withDayOfMonth(1);
LocalDate lastOfMonth = today.withDayOfMonth(today.lengthOfMonth());
// โ
Comparing dates
LocalDate deadline = LocalDate.of(2026, 12, 31);
System.out.println(today.isBefore(deadline)); // true
System.out.println(today.isAfter(deadline)); // false
System.out.println(today.isEqual(deadline)); // false
System.out.println(today.compareTo(deadline)); // negative (today < deadline)
// โ
Checking business day
DayOfWeek dow = today.getDayOfWeek();
boolean isBusinessDay = dow != DayOfWeek.SATURDAY && dow != DayOfWeek.SUNDAY;
System.out.println("Business day: " + isBusinessDay);
// โ
Custom formatting
DateTimeFormatter indianFormat = DateTimeFormatter.ofPattern("dd-MMM-yyyy");
System.out.println(today.format(indianFormat)); // e.g., 20-Mar-2026
}
}LocalTime โ Time Without Date or Zone
LocalTime represents a time of day โ hour, minute, second, nanosecond โ with no date and no time zone. It models the wall-clock time seen on a clock face, independent of any particular day. Perfect for representing store opening hours, shift schedules, alarm times, and daily recurring events that are not tied to a specific calendar date.
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
public class LocalTimeDemo {
public static void main(String[] args) {
// โ
Creating LocalTime
LocalTime now = LocalTime.now(); // Current time
LocalTime morning = LocalTime.of(9, 30); // 09:30:00
LocalTime precise = LocalTime.of(14, 45, 30, 500_000_000); // 14:45:30.5
LocalTime midnight = LocalTime.MIDNIGHT; // 00:00
LocalTime noon = LocalTime.NOON; // 12:00
LocalTime parsed = LocalTime.parse("18:30:00"); // 6:30 PM
// โ
Extracting fields
System.out.println(morning.getHour()); // 9
System.out.println(morning.getMinute()); // 30
System.out.println(morning.getSecond()); // 0
System.out.println(morning.getNano()); // 0
// โ
Time arithmetic
LocalTime breakTime = morning.plusHours(2).plusMinutes(30); // 12:00
LocalTime reminderTime = morning.minusMinutes(15); // 09:15
LocalTime endOfDay = LocalTime.of(17, 0).plusSeconds(3600); // 18:00
// โ
Comparing times
LocalTime openingTime = LocalTime.of(9, 0);
LocalTime closingTime = LocalTime.of(21, 0);
LocalTime visitTime = LocalTime.of(14, 30);
boolean isOpen = visitTime.isAfter(openingTime) && visitTime.isBefore(closingTime);
System.out.println("Store is open: " + isOpen); // true
// โ
Formatting
DateTimeFormatter fmt12h = DateTimeFormatter.ofPattern("hh:mm a");
System.out.println(morning.format(fmt12h)); // 09:30 AM
DateTimeFormatter fmt24h = DateTimeFormatter.ofPattern("HH:mm:ss");
System.out.println(parsed.format(fmt24h)); // 18:30:00
// โ
Truncating precision
LocalTime truncated = LocalTime.now().truncatedTo(java.time.temporal.ChronoUnit.MINUTES);
System.out.println(truncated); // e.g., 14:35 (no seconds/nanos)
}
}LocalDateTime โ Date and Time, No Zone
LocalDateTime is the combination of LocalDate and LocalTime โ it holds a date AND time with no time zone information. It is ideal for recording timestamps within a single time zone context, like a meeting scheduled at a specific date and time within one region, or a database timestamp column where the application always operates in one known time zone.
import java.time.*;
import java.time.format.DateTimeFormatter;
public class LocalDateTimeDemo {
public static void main(String[] args) {
// โ
Creating LocalDateTime
LocalDateTime now = LocalDateTime.now();
LocalDateTime specific = LocalDateTime.of(2026, 8, 15, 10, 30, 0);
LocalDateTime fromParts = LocalDateTime.of(
LocalDate.of(2026, Month.DECEMBER, 31),
LocalTime.of(23, 59, 59)
);
LocalDateTime parsed = LocalDateTime.parse("2026-12-31T23:59:59");
System.out.println(specific); // 2026-08-15T10:30
System.out.println(fromParts); // 2026-12-31T23:59:59
// โ
Extracting components
System.out.println(specific.toLocalDate()); // 2026-08-15
System.out.println(specific.toLocalTime()); // 10:30
System.out.println(specific.getYear()); // 2026
System.out.println(specific.getDayOfWeek()); // SATURDAY
// โ
Immutable arithmetic
LocalDateTime meeting = specific.plusHours(2); // 12:30
LocalDateTime nextReminder = specific.plusDays(7); // Aug 22
LocalDateTime yearEnd = specific.withMonth(12).withDayOfMonth(31);
// โ
Formatting
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("dd MMM yyyy, hh:mm a");
System.out.println(specific.format(formatter));
// Output: 15 Aug 2026, 10:30 AM
// โ
Converting to ZonedDateTime when zone info is needed
ZoneId kolkata = ZoneId.of("Asia/Kolkata");
ZonedDateTime zoned = specific.atZone(kolkata);
System.out.println(zoned); // 2026-08-15T10:30+05:30[Asia/Kolkata]
// โ
Converting to Instant (UTC epoch)
Instant instant = specific.toInstant(ZoneOffset.of("+05:30"));
System.out.println(instant); // 2026-08-15T05:00:00Z
}
}ZonedDateTime โ The Complete Temporal Type
ZonedDateTime is the most complete temporal type in java.time โ it stores a date, time, UTC offset, AND time zone ID. Unlike LocalDateTime, it unambiguously identifies a precise point in time visible to a person in a specific region. It correctly handles Daylight Saving Time (DST) transitions, historical time zone changes, and cross-timezone arithmetic. Use it when your application genuinely needs to reason about time in multiple zones.
import java.time.*;
import java.time.format.DateTimeFormatter;
public class ZonedDateTimeDemo {
public static void main(String[] args) {
// โ
Creating ZonedDateTime
ZoneId kolkata = ZoneId.of("Asia/Kolkata");
ZoneId newYork = ZoneId.of("America/New_York");
ZoneId london = ZoneId.of("Europe/London");
ZoneId utc = ZoneId.of("UTC");
ZonedDateTime nowKolkata = ZonedDateTime.now(kolkata);
ZonedDateTime nowNY = ZonedDateTime.now(newYork);
System.out.println(nowKolkata); // 2026-03-20T15:30:00+05:30[Asia/Kolkata]
System.out.println(nowNY); // 2026-03-20T05:00:00-05:00[America/New_York]
// โ
Creating a specific ZonedDateTime
ZonedDateTime teamCall = ZonedDateTime.of(
LocalDateTime.of(2026, Month.APRIL, 10, 10, 0),
newYork
);
System.out.println("NYC time: " + teamCall);
// โ
Converting between time zones โ same instant, different wall-clock
ZonedDateTime callInKolkata = teamCall.withZoneSameInstant(kolkata);
ZonedDateTime callInLondon = teamCall.withZoneSameInstant(london);
System.out.println("Same call in Kolkata: " + callInKolkata);
// 2026-04-10T19:30+05:30[Asia/Kolkata]
System.out.println("Same call in London: " + callInLondon);
// 2026-04-10T15:00+01:00[Europe/London]
// โ
vs withZoneSameLocal โ changes zone, keeps clock time (different instant)
ZonedDateTime locallyEquivalent = teamCall.withZoneSameLocal(kolkata);
System.out.println("Local equivalent: " + locallyEquivalent);
// 2026-04-10T10:00+05:30[Asia/Kolkata] โ 10:00 Kolkata โ 10:00 NYC
// โ
Listing all available time zones (partial)
ZoneId.getAvailableZoneIds()
.stream()
.filter(z -> z.startsWith("Asia/"))
.sorted()
.limit(5)
.forEach(System.out::println);
// Asia/Aden, Asia/Almaty, Asia/Amman, Asia/Anadyr, Asia/Aqtau
// โ
Formatting for display
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm z");
System.out.println(callInKolkata.format(fmt)); // 10 Apr 2026 19:30 IST
}
}Instant โ The Machine Timestamp
Instant represents a single, specific point on the global timeline โ the number of seconds (and nanoseconds) elapsed since the Unix epoch: January 1, 1970, 00:00:00 UTC. It has no time zone, no DST consideration, no calendar system โ it is the rawest, most machine-centric time value in java.time. Instant is the go-to type for event timestamping, database records, elapsed time measurement, and logging.
import java.time.*;
import java.time.temporal.ChronoUnit;
public class InstantDemo {
public static void main(String[] args) throws InterruptedException {
// โ
Creating Instant
Instant now = Instant.now(); // Current UTC moment
Instant epoch = Instant.EPOCH; // 1970-01-01T00:00:00Z
Instant future = Instant.now().plusSeconds(3600); // 1 hour from now
Instant fromEpoch = Instant.ofEpochSecond(1_700_000_000L); // Unix timestamp
Instant fromMilli = Instant.ofEpochMilli(System.currentTimeMillis());
System.out.println(now); // e.g., 2026-03-20T10:00:00.123456789Z
System.out.println(epoch); // 1970-01-01T00:00:00Z
// โ
Reading Instant values
System.out.println(now.getEpochSecond()); // epoch seconds
System.out.println(now.getNano()); // nanosecond adjustment
System.out.println(now.toEpochMilli()); // milliseconds since epoch
// โ
Measuring elapsed time โ the RIGHT way
Instant start = Instant.now();
Thread.sleep(50); // Simulate some work
Instant end = Instant.now();
Duration elapsed = Duration.between(start, end);
System.out.println("Elapsed: " + elapsed.toMillis() + " ms"); // ~50ms
// โ
Comparing Instants
System.out.println(start.isBefore(end)); // true
System.out.println(start.isAfter(end)); // false
System.out.println(start.compareTo(end)); // negative
// โ
Instant โ ZonedDateTime conversion
ZoneId kolkata = ZoneId.of("Asia/Kolkata");
ZonedDateTime humanReadable = now.atZone(kolkata);
System.out.println(humanReadable);
// 2026-03-20T15:30:00.123456789+05:30[Asia/Kolkata]
// โ
Instant โ java.util.Date (bridge for legacy code)
java.util.Date legacyDate = java.util.Date.from(now);
Instant backToInstant = legacyDate.toInstant();
// โ
Truncating precision (useful for comparisons)
Instant truncatedToSeconds = now.truncatedTo(ChronoUnit.SECONDS);
Instant truncatedToMinutes = now.truncatedTo(ChronoUnit.MINUTES);
}
}Duration vs Period โ Measuring Time Gaps Correctly
Java provides two distinct types for representing an amount of time: Duration for machine-precision time spans, and Period for human-calendar spans. Choosing the wrong one leads to subtle DST-related bugs โ one of the most insidious classes of datetime errors in production systems.
import java.time.*;
import java.time.temporal.ChronoUnit;
public class DurationAndPeriod {
public static void main(String[] args) {
// ============================================================
// DURATION โ time-based amount (seconds + nanoseconds)
// ============================================================
// Creating Duration
Duration twoHours = Duration.ofHours(2);
Duration thirtyMins = Duration.ofMinutes(30);
Duration halfDay = Duration.ofSeconds(43200);
Duration withNanos = Duration.ofSeconds(5, 500_000_000); // 5.5 seconds
// Duration between two time-based values
LocalDateTime start = LocalDateTime.of(2026, 3, 20, 9, 0, 0);
LocalDateTime end = LocalDateTime.of(2026, 3, 20, 17, 30, 0);
Duration workDay = Duration.between(start, end);
System.out.println(workDay); // PT8H30M
System.out.println(workDay.toHours()); // 8
System.out.println(workDay.toMinutes()); // 510
System.out.println(workDay.toMinutesPart()); // 30 (Java 9+)
System.out.println(workDay.toHoursPart()); // 8 (Java 9+)
// Duration arithmetic
Duration overtime = workDay.plusHours(2); // PT10H30M
Duration halfWorkDay = workDay.dividedBy(2); // PT4H15M
boolean isLongDay = workDay.compareTo(Duration.ofHours(10)) > 0;
// ============================================================
// PERIOD โ date-based amount (years, months, days)
// ============================================================
// Creating Period
Period oneYear = Period.ofYears(1);
Period sixMonths = Period.ofMonths(6);
Period threeWeeks = Period.ofWeeks(3); // Stored as 21 days
Period complex = Period.of(1, 6, 15); // 1 year, 6 months, 15 days
// Period between two dates
LocalDate dob = LocalDate.of(1995, Month.JULY, 15);
LocalDate today = LocalDate.of(2026, Month.MARCH, 20);
Period age = Period.between(dob, today);
System.out.println(age); // P30Y8M5D
System.out.println(age.getYears()); // 30
System.out.println(age.getMonths()); // 8
System.out.println(age.getDays()); // 5
System.out.println("Age: " + age.getYears() + " years"); // Age: 30 years
// Period arithmetic
LocalDate subscriptionStart = LocalDate.of(2026, 1, 1);
LocalDate subscriptionEnd = subscriptionStart.plus(Period.ofYears(1));
System.out.println("Subscription ends: " + subscriptionEnd); // 2027-01-01
// โ ๏ธ Critical difference: Duration.ofDays(1) vs Period.ofDays(1) at DST
// Duration.ofDays(1) = always 86400 seconds
// Period.ofDays(1) added to a ZonedDateTime near DST = may be 23 or 25 hours
ZonedDateTime beforeDST = ZonedDateTime.of(
LocalDate.of(2026, 3, 8), LocalTime.of(10, 0),
ZoneId.of("America/New_York")); // DST starts March 8 in US
ZonedDateTime plusOnePeriod = beforeDST.plus(Period.ofDays(1));
ZonedDateTime plusOneDuration = beforeDST.plus(Duration.ofDays(1));
System.out.println("Period +1 day: " + plusOnePeriod.toLocalTime());
// 10:00 โ same wall-clock time next day (DST-aware)
System.out.println("Duration +1 day: " + plusOneDuration.toLocalTime());
// 11:00 โ exactly 86400 seconds later (DST shifted clock forward 1hr)
}
}DateTimeFormatter โ Formatting & Parsing
DateTimeFormatter is the thread-safe, immutable replacement for SimpleDateFormat. It converts between java.time objects and String in both directions: formatting (date object โ string) and parsing (string โ date object). It supports predefined ISO patterns, custom patterns, locale-sensitive formats, and even optional sections.
DateTimeFormatter.ISO_LOCAL_DATE โ '2026-08-15'. ISO_LOCAL_TIME โ '10:30:00'. ISO_LOCAL_DATE_TIME โ '2026-08-15T10:30:00'. ISO_OFFSET_DATE_TIME โ '2026-08-15T10:30:00+05:30'. ISO_INSTANT โ '2026-08-15T05:00:00Z'. ISO_ZONED_DATE_TIME โ '2026-08-15T10:30:00+05:30[Asia/Kolkata]'. These are the defaults used by toString() on java.time objects.
y=year, M=month, d=day, H=hour(0-23), h=hour(1-12), m=minute, s=second, n=nanosecond, a=AM/PM, z=zone name, Z=zone offset, E=day name, D=day of year, w=week of year. Case and repetition matter: M=month digit, MM=zero-padded, MMM=Jan, MMMM=January. H=hour, HH=zero-padded. Always quote literal text with single quotes: 'T' in a pattern is literal T.
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL) produces locale-aware output. With Locale.of('hi', 'IN') you get Hindi format; with Locale.US you get US format. FormatStyle options: FULL ('Thursday, March 20, 2026'), LONG ('March 20, 2026'), MEDIUM ('Mar 20, 2026'), SHORT ('3/20/26'). Use withLocale(Locale) to chain: DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(Locale.UK).
Unlike SimpleDateFormat, DateTimeFormatter is IMMUTABLE and THREAD-SAFE. Store it safely as a private static final field: private static final DateTimeFormatter INDIAN_FORMAT = DateTimeFormatter.ofPattern('dd-MMM-yyyy'); This is the standard pattern in production code โ create once, reuse everywhere, no synchronization needed.
import java.time.*;
import java.time.format.*;
import java.util.Locale;
public class DateTimeFormatterDemo {
// โ
Thread-safe โ store as static final
private static final DateTimeFormatter ISO_DATE = DateTimeFormatter.ISO_LOCAL_DATE;
private static final DateTimeFormatter INDIAN_DATE = DateTimeFormatter.ofPattern("dd-MMM-yyyy");
private static final DateTimeFormatter INVOICE_DT = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
private static final DateTimeFormatter DISPLAY_DT = DateTimeFormatter.ofPattern("EEEE, dd MMMM yyyy 'at' hh:mm a", Locale.ENGLISH);
private static final DateTimeFormatter API_TIMESTAMP = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
public static void main(String[] args) {
LocalDate date = LocalDate.of(2026, 8, 15);
LocalDateTime ldt = LocalDateTime.of(2026, 8, 15, 10, 30, 0);
ZonedDateTime zdt = ldt.atZone(ZoneId.of("Asia/Kolkata"));
// โ
Formatting
System.out.println(date.format(ISO_DATE)); // 2026-08-15
System.out.println(date.format(INDIAN_DATE)); // 15-Aug-2026
System.out.println(ldt.format(INVOICE_DT)); // 15/08/2026 10:30:00
System.out.println(ldt.format(DISPLAY_DT)); // Saturday, 15 August 2026 at 10:30 AM
System.out.println(zdt.format(API_TIMESTAMP)); // 2026-08-15T10:30:00+05:30
// โ
Parsing โ string โ date object
LocalDate parsed1 = LocalDate.parse("15-Aug-2026", INDIAN_DATE);
LocalDate parsed2 = LocalDate.parse("2026-08-15"); // uses ISO default
LocalDateTime parsed3 = LocalDateTime.parse("15/08/2026 10:30:00", INVOICE_DT);
System.out.println(parsed1); // 2026-08-15
System.out.println(parsed3); // 2026-08-15T10:30
// โ
Safe parsing with error handling
String userInput = "not-a-date";
try {
LocalDate result = LocalDate.parse(userInput, INDIAN_DATE);
} catch (DateTimeParseException e) {
System.out.println("Invalid date format: " + e.getMessage());
}
// โ
Locale-sensitive formatting
DateTimeFormatter hindi = DateTimeFormatter
.ofLocalizedDate(FormatStyle.FULL)
.withLocale(new Locale("hi", "IN"));
DateTimeFormatter us = DateTimeFormatter
.ofLocalizedDate(FormatStyle.LONG)
.withLocale(Locale.US);
System.out.println(date.format(hindi)); // เคถเคจเคฟเคตเคพเคฐ, 15 เค
เคเคธเฅเคค 2026
System.out.println(date.format(us)); // August 15, 2026
// โ
Common pattern reference
String[][] patterns = {
{"dd/MM/yyyy", "15/08/2026"},
{"yyyy-MM-dd", "2026-08-15"},
{"dd-MMM-yyyy", "15-Aug-2026"},
{"dd MMMM yyyy", "15 August 2026"},
{"EEEE, dd MMM yyyy", "Saturday, 15 Aug 2026"},
{"HH:mm:ss", "10:30:00"},
{"hh:mm a", "10:30 AM"},
{"dd/MM/yyyy HH:mm:ss", "15/08/2026 10:30:00"}
};
System.out.println("\nPattern Reference:");
for (String[] p : patterns) {
System.out.printf(" %-30s โ %s%n", p[0], p[1]);
}
}
}TemporalAdjusters โ Navigating the Calendar Like a Pro
TemporalAdjusters is a utility class in java.time.temporal providing factory methods for common calendar navigation tasks โ finding the next Monday, the last day of the month, the second Tuesday of the month, and so on. These cover the kinds of date calculations that plague financial, HR, and scheduling systems โ calculations that look simple but have dozens of edge cases.
import java.time.*;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.TemporalAdjuster;
public class TemporalAdjustersDemo {
public static void main(String[] args) {
LocalDate today = LocalDate.of(2026, Month.MARCH, 20); // Thursday
// โ
Built-in adjusters
LocalDate firstOfMonth = today.with(TemporalAdjusters.firstDayOfMonth());
LocalDate lastOfMonth = today.with(TemporalAdjusters.lastDayOfMonth());
LocalDate firstOfYear = today.with(TemporalAdjusters.firstDayOfYear());
LocalDate lastOfYear = today.with(TemporalAdjusters.lastDayOfYear());
LocalDate firstOfNextM = today.with(TemporalAdjusters.firstDayOfNextMonth());
LocalDate firstOfNextY = today.with(TemporalAdjusters.firstDayOfNextYear());
System.out.println("First of month: " + firstOfMonth); // 2026-03-01
System.out.println("Last of month: " + lastOfMonth); // 2026-03-31
System.out.println("First of year: " + firstOfYear); // 2026-01-01
System.out.println("Last of year: " + lastOfYear); // 2026-12-31
// โ
Day-of-week navigation
LocalDate nextMonday = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
LocalDate lastFriday = today.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY));
LocalDate nextOrSameMon = today.with(TemporalAdjusters.nextOrSame(DayOfWeek.THURSDAY));
System.out.println("Next Monday: " + nextMonday); // 2026-03-23
System.out.println("Last Friday: " + lastFriday); // 2026-03-20 (today IS Friday... wait)
System.out.println("Next or same Thu: " + nextOrSameMon); // 2026-03-20 (today is Thursday)
// โ
Nth occurrence โ e.g., second Tuesday of the month (Tax day, meeting schedules)
LocalDate secondTuesdayOfMonth = today.with(
TemporalAdjusters.dayOfWeekInMonth(2, DayOfWeek.TUESDAY));
System.out.println("2nd Tuesday: " + secondTuesdayOfMonth); // 2026-03-10
// โ
Last occurrence โ e.g., last Friday of the month (salary day)
LocalDate lastFridayOfMonth = today.with(
TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY));
System.out.println("Last Friday: " + lastFridayOfMonth); // 2026-03-27
// โ
Custom adjuster โ next business day (skip weekend)
TemporalAdjuster nextBusinessDay = temporal -> {
LocalDate date = LocalDate.from(temporal);
DayOfWeek dow = date.getDayOfWeek();
int daysToAdd = switch (dow) {
case FRIDAY -> 3; // Skip Sat + Sun
case SATURDAY -> 2; // Skip Sun
default -> 1;
};
return date.plusDays(daysToAdd);
};
LocalDate friday = LocalDate.of(2026, 3, 20);
LocalDate nextBiz = friday.with(nextBusinessDay);
System.out.println("Next business day from Friday: " + nextBiz); // 2026-03-23 (Monday)
// โ
Salary payment example: if 25th is weekend, pay on last business day
LocalDate payDay = LocalDate.of(2026, 3, 25); // Wednesday
DayOfWeek payDow = payDay.getDayOfWeek();
if (payDow == DayOfWeek.SATURDAY) payDay = payDay.minusDays(1);
if (payDow == DayOfWeek.SUNDAY) payDay = payDay.minusDays(2);
System.out.println("Salary credited: " + payDay); // 2026-03-25 (Wed โ no change)
}
}ChronoUnit โ Measuring Between Dates with Precision
ChronoUnit is an enum in java.time.temporal representing standard units of time โ from nanoseconds to millennia. Its most practical method is between(temporal1, temporal2), which calculates the number of complete units between two temporal objects. It is the clearest way to answer questions like: 'How many days until the deadline?' or 'How many months has this subscription been active?'
import java.time.*;
import java.time.temporal.ChronoUnit;
public class ChronoUnitDemo {
public static void main(String[] args) {
LocalDate projectStart = LocalDate.of(2026, 1, 1);
LocalDate projectEnd = LocalDate.of(2026, 12, 31);
LocalDate today = LocalDate.of(2026, 3, 20);
// โ
Count between two dates
long totalDays = ChronoUnit.DAYS.between(projectStart, projectEnd);
long weeksLeft = ChronoUnit.WEEKS.between(today, projectEnd);
long monthsLeft = ChronoUnit.MONTHS.between(today, projectEnd);
long daysElapsed = ChronoUnit.DAYS.between(projectStart, today);
System.out.println("Project total days: " + totalDays); // 364
System.out.println("Weeks remaining: " + weeksLeft); // 41
System.out.println("Months remaining: " + monthsLeft); // 9
System.out.println("Days elapsed: " + daysElapsed); // 78
// โ
With LocalDateTime โ time units too
LocalDateTime meetingStart = LocalDateTime.of(2026, 3, 20, 9, 0, 0);
LocalDateTime meetingEnd = LocalDateTime.of(2026, 3, 20, 11, 45, 30);
long hours = ChronoUnit.HOURS.between(meetingStart, meetingEnd);
long minutes = ChronoUnit.MINUTES.between(meetingStart, meetingEnd);
long seconds = ChronoUnit.SECONDS.between(meetingStart, meetingEnd);
System.out.println("Meeting hours: " + hours); // 2
System.out.println("Meeting minutes: " + minutes); // 165
System.out.println("Meeting seconds: " + seconds); // 9930
// โ
Age calculation โ complete years
LocalDate birthday = LocalDate.of(1995, 7, 15);
long age = ChronoUnit.YEARS.between(birthday, today);
System.out.println("Age: " + age + " years"); // Age: 30 years
// โ
Days until next birthday
LocalDate nextBirthday = birthday.withYear(today.getYear());
if (nextBirthday.isBefore(today) || nextBirthday.isEqual(today)) {
nextBirthday = nextBirthday.plusYears(1);
}
long daysUntilBirthday = ChronoUnit.DAYS.between(today, nextBirthday);
System.out.println("Days to birthday: " + daysUntilBirthday);
// โ
All ChronoUnit values
System.out.println("\nAll ChronoUnits:");
for (ChronoUnit unit : ChronoUnit.values()) {
System.out.printf(" %-15s โ estimated duration: %s%n",
unit, unit.getDuration());
}
}
}Time Zone Handling & DST Pitfalls โ The Hard Part
Time zones are the most complex part of any date/time system. The IANA Time Zone Database (also called the tz or zoneinfo database) is Java's source of truth โ accessed via ZoneId. It defines over 600 named zones, each with its complete history of UTC offsets and DST transitions. Getting time zones right in production requires understanding three common failure modes: DST gaps, DST overlaps, and offset vs zone ID.
import java.time.*;
import java.time.format.DateTimeFormatter;
public class TimeZoneHandling {
public static void main(String[] args) {
// โ
ZoneId vs ZoneOffset โ know the difference
ZoneId kolkataZoneId = ZoneId.of("Asia/Kolkata"); // Named zone โ full DST history
ZoneOffset fixedOffset = ZoneOffset.of("+05:30"); // Fixed offset โ no DST logic
ZoneOffset utcOffset = ZoneOffset.UTC; // UTC โ zero offset
// India doesn't observe DST โ so both are equivalent for IST
// But for America/New_York (which does DST), they behave differently:
ZoneId nyZoneId = ZoneId.of("America/New_York"); // -05:00 or -04:00 (DST)
ZoneOffset nyFixedOffset = ZoneOffset.of("-05:00"); // Always -5 (ignores DST)
// โ
DST GAP โ 'Spring Forward': an hour that doesn't exist
// US Eastern DST starts: 2026-03-08 02:00 โ jumps to 03:00
// So 02:30 AM on March 8 does NOT EXIST in America/New_York
LocalDateTime gapTime = LocalDateTime.of(2026, 3, 8, 2, 30);
ZonedDateTime gapZoned = gapTime.atZone(nyZoneId);
System.out.println("Gap time resolved: " + gapZoned);
// Java auto-adjusts: 2026-03-08T03:30-04:00[America/New_York]
// โ
DST OVERLAP โ 'Fall Back': an hour that appears twice
// US Eastern DST ends: 2026-11-01 02:00 โ falls back to 01:00
// So 01:30 AM on Nov 1 is AMBIGUOUS โ could be -04:00 or -05:00
LocalDateTime overlapTime = LocalDateTime.of(2026, 11, 1, 1, 30);
ZonedDateTime overlapEarlier = overlapTime.atZone(nyZoneId); // Default: earlier (DST)
ZonedDateTime overlapLater = overlapTime.atZone(nyZoneId)
.withLaterOffsetAtOverlap(); // Explicitly choose later (standard)
System.out.println("Overlap earlier: " + overlapEarlier);
// 2026-11-01T01:30-04:00[America/New_York]
System.out.println("Overlap later: " + overlapLater);
// 2026-11-01T01:30-05:00[America/New_York]
// โ
Best practice: store as Instant, display in user's zone
Instant eventInstant = Instant.parse("2026-04-10T09:00:00Z"); // Stored in DB
String[] userZones = {"Asia/Kolkata", "America/New_York", "Europe/London", "Australia/Sydney"};
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm z");
System.out.println("\nGlobal display of the same event:");
for (String zone : userZones) {
ZonedDateTime userView = eventInstant.atZone(ZoneId.of(zone));
System.out.printf(" %-20s โ %s%n", zone, userView.format(fmt));
}
// Asia/Kolkata โ 10 Apr 2026 14:30 IST
// America/New_York โ 10 Apr 2026 05:00 EDT
// Europe/London โ 10 Apr 2026 10:00 BST
// Australia/Sydney โ 10 Apr 2026 20:00 AEST
}
}Migrating from Legacy Date/Calendar to java.time
If you work in any codebase more than a few years old, you will encounter java.util.Date, java.util.Calendar, and java.sql.Timestamp. Java provides official bridge methods on the legacy classes to convert to and from java.time types โ enabling incremental migration without rewriting everything at once.
import java.time.*;
import java.util.Calendar;
import java.util.TimeZone;
public class LegacyMigration {
public static void main(String[] args) {
// ============================================================
// java.util.Date โโ java.time
// ============================================================
// โ
Date โ Instant (the primary bridge)
java.util.Date legacyDate = new java.util.Date();
Instant instant = legacyDate.toInstant();
System.out.println("Instant: " + instant);
// โ
Instant โ Date (for legacy APIs that still require Date)
java.util.Date backToDate = java.util.Date.from(instant);
// โ
Date โ LocalDateTime (via Instant + ZoneId)
LocalDateTime ldt = instant
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
// โ
LocalDateTime โ Date (for legacy output)
java.util.Date fromLdt = java.util.Date.from(
ldt.atZone(ZoneId.systemDefault()).toInstant());
// ============================================================
// java.util.Calendar โโ java.time
// ============================================================
// โ
Calendar โ Instant
Calendar cal = Calendar.getInstance();
Instant calInstant = cal.toInstant();
// โ
Calendar โ ZonedDateTime
TimeZone tz = cal.getTimeZone();
ZoneId zoneId = tz.toZoneId();
ZonedDateTime zdt = calInstant.atZone(zoneId);
System.out.println("ZonedDateTime: " + zdt);
// ============================================================
// java.sql.Date โโ LocalDate (JDBC)
// ============================================================
// โ
sql.Date โ LocalDate
java.sql.Date sqlDate = java.sql.Date.valueOf("2026-08-15");
LocalDate localDate = sqlDate.toLocalDate();
System.out.println("LocalDate: " + localDate);
// โ
LocalDate โ sql.Date
java.sql.Date backToSql = java.sql.Date.valueOf(localDate);
// ============================================================
// java.sql.Timestamp โโ LocalDateTime / Instant (JDBC)
// ============================================================
// โ
Timestamp โ LocalDateTime
java.sql.Timestamp ts = new java.sql.Timestamp(System.currentTimeMillis());
LocalDateTime fromTs = ts.toLocalDateTime();
System.out.println("LocalDateTime: " + fromTs);
// โ
Timestamp โ Instant
Instant tsInstant = ts.toInstant();
// โ
LocalDateTime โ Timestamp
java.sql.Timestamp backToTs = java.sql.Timestamp.valueOf(LocalDateTime.now());
// ============================================================
// Cheat Sheet Summary
// ============================================================
System.out.println("\n=== Migration Cheat Sheet ===");
System.out.println("Date โ Instant: date.toInstant()");
System.out.println("Instant โ Date: Date.from(instant)");
System.out.println("Calendar โ Instant: cal.toInstant()");
System.out.println("Calendar โ ZoneId: cal.getTimeZone().toZoneId()");
System.out.println("sql.Date โ LocalDate: sqlDate.toLocalDate()");
System.out.println("LocalDate โ sql.Date: sql.Date.valueOf(ld)");
System.out.println("Timestamp โ LocalDateTime: ts.toLocalDateTime()");
System.out.println("LocalDateTime โ Timestamp: Timestamp.valueOf(ldt)");
}
}Bad Practices & Anti-Patterns โ What Causes Production Incidents
Date/time bugs are disproportionately responsible for production incidents โ they are hard to reproduce (depend on the current time, JVM timezone, or DST state), silent (no exception, just wrong data), and costly (incorrect billing, missed deadlines, corrupted records). These are the most common anti-patterns found in enterprise Java codebases.
Scheduling a meeting at LocalDateTime.of(2026, 4, 10, 10, 0) and sending it to participants in different time zones is a silent bug. LocalDateTime has no zone โ it cannot represent a global moment. Use ZonedDateTime or Instant for any event involving multiple time zones. The meeting in NYC at 10:00 AM is 19:30 IST โ LocalDateTime captures neither correctly.
LocalDateTime.now() and ZonedDateTime.now() without an explicit ZoneId use ZoneId.systemDefault() โ which varies between developer machines, CI servers, and production servers. A JVM arg change, Docker base image update, or server migration can silently shift all timestamps. Always be explicit: LocalDateTime.now(ZoneId.of('Asia/Kolkata')). Never assume the JVM's default zone.
private static final SimpleDateFormat SDF = new SimpleDateFormat('dd/MM/yyyy') is a classic thread-safety bug. SimpleDateFormat uses internal state during format/parse โ concurrent calls corrupt each other's output. Fix: use DateTimeFormatter (thread-safe), or create a new SimpleDateFormat per call, or use ThreadLocal<SimpleDateFormat>.
Using epoch milliseconds (System.currentTimeMillis()) directly in business logic โ storing them in databases, sending them in APIs, comparing them โ makes your code opaque and error-prone. Use Instant, LocalDate, or ZonedDateTime which carry semantic meaning. When you see 1711929600000L you need to decode it; when you see 2024-04-01T00:00:00Z you understand it instantly.
new Date() == new Date() is always false (reference comparison). date1.equals(date2) compares millisecond precision โ two 'same day' dates with different times won't be equal. For same-day comparison: date1.toLocalDate().isEqual(date2.toLocalDate()). For range checks: always use isBefore/isAfter on LocalDate or compareTo on Instant.
On DST transition days, adding 24 hours to a ZonedDateTime gives a different wall-clock time than 'tomorrow'. Use Period.ofDays(1) (calendar-aware) not Duration.ofDays(1) (exactly 86400 seconds) when you mean 'the same time tomorrow'. Example: adding Duration.ofDays(1) to 10:00 AM on the night clocks go forward gives 11:00 AM โ not 10:00 AM as a user would expect.
Real-World Production Code Examples โ Date/Time in Enterprise Java
The following examples model date/time handling patterns in production Spring Boot microservices โ covering subscription management, global event scheduling, and audit logging.
package com.techsustainify.subscription.service;
import java.time.*;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
public class SubscriptionService {
private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Kolkata");
/**
* Calculate subscription end date.
* Uses Period for calendar-accurate month addition (Feb edge cases handled).
*/
public LocalDate calculateEndDate(LocalDate startDate, int durationMonths) {
return startDate.plus(Period.ofMonths(durationMonths));
// Period handles Feb correctly:
// Jan 31 + 1 month = Feb 28 (not Feb 31 which doesn't exist)
}
/**
* Check if subscription is within its grace period (7 days after expiry).
*/
public boolean isInGracePeriod(LocalDate expiryDate) {
LocalDate today = LocalDate.now(BUSINESS_ZONE);
long daysPastExpiry = ChronoUnit.DAYS.between(expiryDate, today);
return daysPastExpiry > 0 && daysPastExpiry <= 7;
}
/**
* Get next billing date.
* If billing day is 31 and next month has fewer days, use last day.
*/
public LocalDate getNextBillingDate(LocalDate lastBillingDate, int billingDayOfMonth) {
LocalDate nextMonth = lastBillingDate.plusMonths(1)
.withDayOfMonth(1); // First of next month
int maxDay = nextMonth.lengthOfMonth();
int actualDay = Math.min(billingDayOfMonth, maxDay);
return nextMonth.withDayOfMonth(actualDay);
}
/**
* Check if renewal reminder should be sent (7 days before expiry,
* but not on weekends โ push to nearest weekday).
*/
public LocalDate getReminderSendDate(LocalDate expiryDate) {
LocalDate reminderDate = expiryDate.minusDays(7);
DayOfWeek dow = reminderDate.getDayOfWeek();
if (dow == DayOfWeek.SATURDAY) return reminderDate.minusDays(1); // Friday
if (dow == DayOfWeek.SUNDAY) return reminderDate.minusDays(2); // Friday
return reminderDate;
}
/**
* Calculate age of account in human-readable form.
*/
public String getAccountAge(LocalDate createdDate) {
LocalDate today = LocalDate.now(BUSINESS_ZONE);
Period age = Period.between(createdDate, today);
long daysDiff = ChronoUnit.DAYS.between(createdDate, today);
if (daysDiff < 30) return daysDiff + " days";
if (age.getYears() == 0) return age.getMonths() + " months";
return age.getYears() + " years, " + age.getMonths() + " months";
}
}package com.techsustainify.audit.service;
import java.time.*;
import java.time.format.DateTimeFormatter;
/**
* Production audit logging โ demonstrates correct Instant usage
* for machine timestamps and ZonedDateTime for human display.
*/
public class AuditLogger {
// Thread-safe formatters โ defined once, reused everywhere
private static final DateTimeFormatter LOG_FORMAT =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
private static final DateTimeFormatter DISPLAY_FORMAT =
DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm:ss z");
private static final ZoneId IST = ZoneId.of("Asia/Kolkata");
public record AuditEntry(
String eventType,
String userId,
String resourceId,
Instant occurredAt, // Store as Instant โ machine-precise, UTC, immutable
String ipAddress,
boolean success
) {
/** Display timestamp in IST for Indian operations team */
public String getDisplayTime() {
return occurredAt.atZone(IST).format(DISPLAY_FORMAT);
}
/** ISO-8601 timestamp for API responses */
public String getIsoTimestamp() {
return occurredAt.atZone(ZoneOffset.UTC).format(LOG_FORMAT);
}
/** How long ago did this event occur? */
public String getRelativeTime() {
Duration since = Duration.between(occurredAt, Instant.now());
if (since.toSeconds() < 60) return since.toSeconds() + " seconds ago";
if (since.toMinutes() < 60) return since.toMinutes() + " minutes ago";
if (since.toHours() < 24) return since.toHours() + " hours ago";
return ChronoUnit.DAYS.between(occurredAt, Instant.now()) + " days ago";
}
}
public AuditEntry logEvent(String eventType, String userId, String resourceId,
String ipAddress, boolean success) {
return new AuditEntry(
eventType, userId, resourceId,
Instant.now(), // Always Instant.now() โ never new Date()
ipAddress, success
);
}
// โ
Calculating SLA breach
public boolean isSlaBreached(Instant ticketCreatedAt, Duration slaDuration) {
Instant slaDeadline = ticketCreatedAt.plus(slaDuration);
return Instant.now().isAfter(slaDeadline);
}
// โ
Convert stored Instant to user's local timezone for display
public String toUserTimezone(Instant instant, String userTimezone) {
ZoneId zone = ZoneId.of(userTimezone);
return instant.atZone(zone).format(DISPLAY_FORMAT);
}
}Which Date Type Should I Use? โ Decision Guide
Use this decision guide to instantly identify the right java.time type for your specific requirement.
Code Execution Flow โ from source to output
Java Date & Time Interview Questions โ Beginner to Advanced
These questions are asked in Java developer interviews from junior to senior level, in Spring Boot technical rounds, and in backend architect assessments.
Practice Questions โ Test Your Date & Time Knowledge
Attempt each question independently before reading the answer. These questions test real-world date/time reasoning โ the exact scenarios you'll encounter in production Java development.
1. What is the output of this code? LocalDate d = LocalDate.of(2026, 1, 31); LocalDate result = d.plusMonths(1); System.out.println(result);
Medium2. What is wrong with this code? How do you fix it? private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("dd-MMM-yyyy"); public String formatDate(Date date) { return FORMATTER.format(date); }
Easy3. Calculate and print the number of days, hours, and minutes a user session lasted: Instant loginTime = Instant.parse('2026-03-20T08:30:00Z'); Instant logoutTime = Instant.parse('2026-03-20T17:45:30Z');
Easy4. A flight departs Mumbai at 22:00 IST on March 20, 2026, and arrives in London at 05:00 GMT+1 (BST) on March 21, 2026. Calculate the flight duration.
Hard5. Write a method that returns all Mondays in a given month and year.
Medium6. What does this print, and why? LocalDate date = LocalDate.of(2026, 3, 20); date.plusDays(10); System.out.println(date);
Easy7. Is Period.between() correct for calculating total days? Why or why not? LocalDate start = LocalDate.of(2026, 1, 1); LocalDate end = LocalDate.of(2026, 3, 20); System.out.println(Period.between(start, end).getDays()); System.out.println(ChronoUnit.DAYS.between(start, end));
Hard8. Design a method to check if two Instant values represent the same calendar day in India (Asia/Kolkata).
HardConclusion โ Master Dates, Master Production Reliability
Date and time handling is one of the most underestimated sources of production incidents in enterprise software. Incorrect timezone conversions cause billing on the wrong day. Daylight saving bugs cause missed alerts. Thread-unsafe formatters corrupt financial records. The legacy java.util.Date and Calendar APIs โ with their zero-indexed months, mutability, and DST blindness โ were responsible for countless production bugs over two decades. java.time eliminates all of these by design.
The hallmark of a production-ready Java developer is using the right type for each temporal concept: LocalDate for date-only data, Instant for machine timestamps, ZonedDateTime for human-facing cross-timezone events, Period for calendar-aware spans, and DateTimeFormatter (never SimpleDateFormat) for all formatting needs.
Your next step: Java Exceptions โ where you'll learn how to handle the errors that arise when date parsing fails, invalid dates are constructed, or timezone operations throw unexpected exceptions. Robust error handling around date operations is the final piece of production-ready date/time code. โ