โ˜• Java

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.

1๏ธโƒฃ
Generation 1 โ€” java.util.Date (Java 1.0, 1995)

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.

2๏ธโƒฃ
Generation 2 โ€” java.util.Calendar (Java 1.1, 1997)

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.

3๏ธโƒฃ
Generation 3 โ€” java.time (Java 8, 2014, JSR-310)

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.

โ˜• JavaLegacyDateProblems.java โ€” The 7 Design Failures
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.

TypePackageStoresHas Time Zone?Typical Use Case
LocalDatejava.timeDate only (y-m-d)โŒ NoBirthdays, holidays, deadlines, business days
LocalTimejava.timeTime only (h-m-s-ns)โŒ NoStore opening hours, alarm times, daily schedules
LocalDateTimejava.timeDate + TimeโŒ NoSingle-timezone scheduling, DB timestamps without zone
ZonedDateTimejava.timeDate + Time + Zoneโœ… YesCross-timezone meetings, flight schedules, global events
OffsetDateTimejava.timeDate + Time + UTC offsetโœ… YesDatabase storage, REST API serialization (ISO-8601)
Instantjava.timeEpoch seconds + nanoseconds (UTC)โœ… UTCEvent timestamps, DB created_at, measuring durations
Durationjava.timeAmount of time in seconds + nanosN/ATime between two time-based events
Periodjava.timeAmount of time in years-months-daysN/AAge calculation, subscription length
ZoneIdjava.timeIANA timezone identifierN/A'Asia/Kolkata', 'America/New_York'
ZoneOffsetjava.timeFixed UTC offsetN/A'+05:30', '-08:00'
MonthDayjava.timeMonth + Day (no year)โŒ NoAnnual recurring events (anniversaries, festivals)
YearMonthjava.timeYear + Month (no day)โŒ NoCredit card expiry, billing month
DateTimeFormatterjava.time.formatFormatting/parsing patternN/AParsing and formatting all java.time types

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.

โ˜• JavaLocalDateDemo.java
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.

โ˜• JavaLocalTimeDemo.java
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.

โ˜• JavaLocalDateTimeDemo.java
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.

โ˜• JavaZonedDateTimeDemo.java
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.

โ˜• JavaInstantDemo.java
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.

AspectDurationPeriod
UnitSeconds + nanosecondsYears + months + days
Works withLocalTime, LocalDateTime, InstantLocalDate
DST aware?โŒ No โ€” 1 day = always 86,400 secondsโœ… Yes โ€” 1 day = 'next calendar day'
PrecisionNanosecondDay (no time component)
CreationDuration.of(), between(), ofHours(), ofMinutes()Period.of(), between(), ofYears(), ofMonths()
ExampleDuration.ofHours(2) = exactly 7,200 secondsPeriod.ofMonths(1) = '1 month' (28-31 days)
Use caseCode benchmarking, SLA timers, connection timeoutsAge calculation, subscription expiry, billing cycles
Negative allowed?โœ… Yes โ€” negative Duration is validโœ… Yes โ€” Period can be negative
โ˜• JavaDurationAndPeriod.java
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.

๐Ÿ“
Predefined ISO Formatters

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.

๐ŸŽจ
Pattern Letters โ€” Custom Formats

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.

๐ŸŒ
Locale-Sensitive Formatters

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).

๐Ÿ”’
Thread Safety โ€” Static Constants Are Safe

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.

โ˜• JavaDateTimeFormatterDemo.java
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.

โ˜• JavaTemporalAdjustersDemo.java
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?'

โ˜• JavaChronoUnitDemo.java
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.

โ˜• JavaTimeZoneHandling.java
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.

โ˜• JavaLegacyMigration.java โ€” Bridge Methods
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.

๐Ÿšซ
Using LocalDateTime for Cross-Timezone Operations

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.

๐Ÿšซ
Relying on JVM Default Timezone

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.

๐Ÿšซ
Using SimpleDateFormat as a Shared Static Field

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>.

๐Ÿšซ
Storing java.util.Date.getTime() as Business Data

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.

๐Ÿšซ
Comparing Dates with == or Ignoring Time Component

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.

๐Ÿšซ
Ignoring DST During 'Add One Day' Operations

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.

โ˜• JavaSubscriptionService.java โ€” Billing Cycle Date Logic
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";
    }
}
โ˜• JavaAuditLogger.java โ€” Timestamp Best Practices
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.

๐Ÿ“‹ What do you need to represent?
Start
๐Ÿ“… Date only? (no time needed)birthday, deadline, holiday
YES
๐Ÿ•’ Time only? (no date needed)opening hour, alarm, shift
YES
๐ŸŒ Does it need a time zone?cross-region / global event?
YES โ€” has zone
โšก Machine timestamp / precise epoch?DB record, logging, SLA timer
YES โ€” machine time
๐Ÿ“ Amount of time (a gap / span)?duration, age, countdown
YES โ€” span of time
๐Ÿ“† LocalDatejava.time.LocalDate
๐Ÿ•‘ LocalTimejava.time.LocalTime
๐ŸŒ ZonedDateTimejava.time.ZonedDateTime
๐Ÿ“‹ LocalDateTimejava.time.LocalDateTime
โš™๏ธ Instantjava.time.Instant (UTC epoch)
โณ Duration / PeriodDuration (time) or Period (calendar)

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);

Medium

2. 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); }

Easy

3. 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');

Easy

4. 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.

Hard

5. Write a method that returns all Mondays in a given month and year.

Medium

6. What does this print, and why? LocalDate date = LocalDate.of(2026, 3, 20); date.plusDays(10); System.out.println(date);

Easy

7. 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));

Hard

8. Design a method to check if two Instant values represent the same calendar day in India (Asia/Kolkata).

Hard

Conclusion โ€” 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.

Use CaseWrong ApproachCorrect Approach
Store a birthdayjava.util.DateLocalDate
Database timestampjava.sql.TimestampInstant โ†’ mapped via JDBC 4.2
User's meeting time (IST)LocalDateTime.now()ZonedDateTime.now(ZoneId.of('Asia/Kolkata'))
Format a date (multi-thread)static SimpleDateFormatstatic final DateTimeFormatter
Calculate agedate1 - date2 in millisPeriod.between(dob, today).getYears()
Time a code blockSystem.currentTimeMillis()Duration.between(Instant.now(), Instant.now())
Add 1 month to Jan 31manual edge-case checkdate.plusMonths(1) โ€” auto-adjusts to Feb 28
Next business dayif/else chain with CalendarCustom TemporalAdjuster
Cross-timezone conversionManual hour mathzdt.withZoneSameInstant(targetZone)
'Add 1 day' respecting DSTDuration.ofDays(1)Period.ofDays(1) on ZonedDateTime

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. โ˜•

Frequently Asked Questions โ€” Java Date & Time