Contents
- Why java.time
- LocalDate
- LocalTime
- LocalDateTime
- ZonedDateTime & OffsetDateTime
- Instant
- Duration & Period
- DateTimeFormatter
- TemporalAdjusters
- Legacy Interop & Best Practices
Before Java 8, developers relied on java.util.Date and java.util.Calendar for date/time work. These classes had fundamental design flaws that made them error-prone and frustrating to use:
- Mutable — Date objects can be changed after creation, making them unsafe to share across threads or pass between methods without defensive copying.
- Not thread-safe — SimpleDateFormat is notoriously non-thread-safe, leading to subtle bugs in concurrent applications.
- Poor API design — months are zero-indexed (January = 0), year requires adding 1900, and Date confusingly represents both a date and a timestamp.
- No time zone support in Date — Date stores only a millisecond offset from epoch, yet its toString() prints the JVM's default time zone, misleading developers.
// Legacy API pitfalls
Date date = new Date(2024, 3, 15); // Actually year 3924, month April (0-indexed!)
Calendar cal = Calendar.getInstance();
cal.set(Calendar.MONTH, 3); // This is April, not March
// Mutable — anyone holding a reference can silently change your date
date.setTime(0L); // Now it's epoch, Jan 1 1970
The java.time package (JSR 310) was designed by Stephen Colebourne, the creator of Joda-Time, working alongside Oracle. It adopts the best ideas from Joda-Time while fixing its shortcomings. Every class in java.time follows these principles:
- Immutable — all instances are final and cannot be modified after creation. Methods like plusDays() return a new object.
- Thread-safe — immutability guarantees safe sharing across threads without synchronization.
- Clear separation — distinct types for date-only (LocalDate), time-only (LocalTime), date+time (LocalDateTime), zoned (ZonedDateTime), and machine timestamps (Instant).
- Fluent API — method chaining with consistent naming: of(), from(), parse(), with(), plus(), minus(), to().
Never use java.util.Date or Calendar in new code. The entire java.time package is available from Java 8 onwards, and back-ported to Java 6/7 via the ThreeTen-Backport library.
LocalDate represents a date without time or time zone — a year, month, and day. It is the right choice for birthdays, holidays, contract dates, or any value where the time of day is irrelevant.
import java.time.LocalDate;
import java.time.Month;
import java.time.DayOfWeek;
// Creating LocalDate instances
LocalDate today = LocalDate.now(); // e.g. 2024-11-20
LocalDate specific = LocalDate.of(2024, 3, 15); // 2024-03-15
LocalDate withEnum = LocalDate.of(2024, Month.MARCH, 15);
LocalDate parsed = LocalDate.parse("2024-03-15"); // ISO format
// Extracting parts
int year = today.getYear(); // 2024
Month month = today.getMonth(); // NOVEMBER (enum)
int monthValue = today.getMonthValue(); // 11 (1-indexed, unlike legacy API!)
int dayOfMonth = today.getDayOfMonth(); // 20
DayOfWeek dow = today.getDayOfWeek(); // WEDNESDAY
int dayOfYear = today.getDayOfYear(); // 325
// Date arithmetic — always returns a NEW LocalDate
LocalDate nextWeek = today.plusDays(7);
LocalDate lastMonth = today.minusMonths(1);
LocalDate nextYear = today.plusYears(1);
// Comparisons
boolean isBefore = specific.isBefore(today); // true
boolean isAfter = specific.isAfter(today); // false
boolean isEqual = specific.isEqual(LocalDate.of(2024, 3, 15)); // true
// Useful utility methods
int daysInMonth = today.lengthOfMonth(); // 30 (November)
int daysInYear = today.lengthOfYear(); // 366 (2024 is a leap year)
boolean leap = today.isLeapYear(); // true
// Replacing parts with 'with' methods
LocalDate firstOfMonth = today.withDayOfMonth(1); // 2024-11-01
LocalDate inJuly = today.withMonth(7); // 2024-07-20
Months in java.time are 1-indexed: January = 1, December = 12. This eliminates the most common bug from the legacy Calendar API where January was 0.
LocalTime represents a time of day without a date or time zone — hours, minutes, seconds, and nanoseconds. Use it for store opening hours, alarm times, or any time-of-day concept.
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
// Creating LocalTime instances
LocalTime now = LocalTime.now(); // e.g. 14:30:45.123456789
LocalTime morning = LocalTime.of(9, 30); // 09:30
LocalTime precise = LocalTime.of(14, 30, 45); // 14:30:45
LocalTime withNanos = LocalTime.of(14, 30, 45, 500_000_000); // 14:30:45.5
LocalTime parsed = LocalTime.parse("09:30:00"); // 09:30
// Useful constants
LocalTime midnight = LocalTime.MIDNIGHT; // 00:00
LocalTime noon = LocalTime.NOON; // 12:00
LocalTime max = LocalTime.MAX; // 23:59:59.999999999
LocalTime min = LocalTime.MIN; // 00:00
// Extracting parts
int hour = now.getHour(); // 0-23
int minute = now.getMinute(); // 0-59
int second = now.getSecond(); // 0-59
int nano = now.getNano(); // 0-999999999
// Arithmetic
LocalTime later = now.plusHours(2).plusMinutes(30);
LocalTime earlier = now.minusMinutes(45);
// Truncation — very useful for rounding
LocalTime toMinutes = now.truncatedTo(ChronoUnit.MINUTES); // 14:30
LocalTime toHours = now.truncatedTo(ChronoUnit.HOURS); // 14:00
// Comparisons
boolean isOpen = now.isAfter(LocalTime.of(9, 0))
&& now.isBefore(LocalTime.of(17, 0)); // business hours check
LocalTime has nanosecond precision (9 decimal places), but most system clocks only provide microsecond or millisecond resolution. Use truncatedTo(ChronoUnit.MILLIS) when you need consistent precision for comparisons or storage.
LocalDateTime combines a LocalDate and a LocalTime into a single object. It represents a date-time without any time zone information. Use it for local events, scheduling within a single time zone, or log timestamps where the zone is implied.
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
// Creating LocalDateTime instances
LocalDateTime now = LocalDateTime.now();
LocalDateTime specific = LocalDateTime.of(2024, 12, 25, 10, 30); // Christmas morning
LocalDateTime parsed = LocalDateTime.parse("2024-12-25T10:30:00"); // ISO format
// Combining from LocalDate and LocalTime
LocalDate date = LocalDate.of(2024, 12, 25);
LocalTime time = LocalTime.of(10, 30);
LocalDateTime combined = LocalDateTime.of(date, time);
LocalDateTime atTime = date.atTime(10, 30); // same result
LocalDateTime atDate = time.atDate(date); // same result
// Extracting the date and time parts
LocalDate datePart = now.toLocalDate();
LocalTime timePart = now.toLocalTime();
// Arithmetic works the same way
LocalDateTime meeting = now.plusDays(3).withHour(14).withMinute(0);
// Practical example: scheduling
LocalDateTime deadline = LocalDateTime.of(2024, 12, 31, 23, 59, 59);
boolean isOverdue = LocalDateTime.now().isAfter(deadline);
LocalDateTime does NOT represent a specific moment in time. The same LocalDateTime of "2024-12-25T10:30" means a different instant in New York vs. Tokyo. If you need to track a precise moment (API timestamps, event logs across time zones, database records), use ZonedDateTime, OffsetDateTime, or Instant instead.
When you need a date-time anchored to a specific time zone, use ZonedDateTime. It wraps a LocalDateTime with a ZoneId and handles daylight saving time (DST) transitions automatically. OffsetDateTime is similar but uses a fixed UTC offset instead of a named zone, making it ideal for database storage and wire protocols.
import java.time.*;
// Creating ZonedDateTime
ZonedDateTime nowNY = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime nowTokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
ZonedDateTime nowUTC = ZonedDateTime.now(ZoneOffset.UTC);
// From a LocalDateTime + zone
LocalDateTime ldt = LocalDateTime.of(2024, 7, 4, 12, 0);
ZonedDateTime july4 = ldt.atZone(ZoneId.of("America/New_York"));
// 2024-07-04T12:00-04:00[America/New_York] (EDT in summer)
// Converting between time zones
ZonedDateTime inLondon = july4.withZoneSameInstant(ZoneId.of("Europe/London"));
// 2024-07-04T17:00+01:00[Europe/London] (BST in summer)
// OffsetDateTime — fixed offset, no DST rules
OffsetDateTime odt = OffsetDateTime.now(ZoneOffset.ofHours(5)); // UTC+05:00
OffsetDateTime fromZoned = july4.toOffsetDateTime();
// Checking available zone IDs
Set<String> zones = ZoneId.getAvailableZoneIds(); // ~600 zones
Common time zone IDs you will use frequently:
| Zone ID | Description | UTC Offset (approx.) |
| America/New_York | US Eastern | UTC-5 / UTC-4 (DST) |
| America/Chicago | US Central | UTC-6 / UTC-5 (DST) |
| America/Denver | US Mountain | UTC-7 / UTC-6 (DST) |
| America/Los_Angeles | US Pacific | UTC-8 / UTC-7 (DST) |
| Europe/London | UK / GMT / BST | UTC+0 / UTC+1 (DST) |
| Europe/Paris | Central Europe | UTC+1 / UTC+2 (DST) |
| Asia/Tokyo | Japan (no DST) | UTC+9 |
| Asia/Kolkata | India (no DST) | UTC+5:30 |
| Australia/Sydney | Australia Eastern | UTC+10 / UTC+11 (DST) |
| UTC | Coordinated Universal Time | UTC+0 |
DST transition example:
// US Eastern: clocks spring forward on March 10, 2024 at 2:00 AM
// 2:30 AM does not exist — it jumps from 1:59 AM to 3:00 AM
LocalDateTime gapTime = LocalDateTime.of(2024, 3, 10, 2, 30);
ZonedDateTime adjusted = gapTime.atZone(ZoneId.of("America/New_York"));
// Adjusted to 2024-03-10T03:30-04:00[America/New_York] (pushed forward)
// US Eastern: clocks fall back on November 3, 2024 at 2:00 AM
// 1:30 AM exists TWICE — once in EDT and once in EST
LocalDateTime overlapTime = LocalDateTime.of(2024, 11, 3, 1, 30);
ZonedDateTime earlier = overlapTime.atZone(ZoneId.of("America/New_York"));
// 2024-11-03T01:30-04:00[America/New_York] (EDT, first occurrence)
ZonedDateTime later = earlier.withLaterOffsetAtOverlap();
// 2024-11-03T01:30-05:00[America/New_York] (EST, second occurrence)
DST transitions can cause surprises. When clocks spring forward, a LocalDateTime in the gap (e.g., 2:30 AM on spring-forward day) will be adjusted automatically by atZone(). When clocks fall back, the same local time exists twice — ZonedDateTime picks the earlier occurrence by default. Use withLaterOffsetAtOverlap() or withEarlierOffsetAtOverlap() to control which one you get. Always test your code around DST boundaries.
When to use which:
- ZonedDateTime — user-facing applications where you need DST-aware scheduling (meeting planners, calendars, notifications).
- OffsetDateTime — database storage, REST APIs, and serialization where a fixed offset is cleaner and DST rules are not needed.
Instant represents a single point on the timeline — the number of seconds (and nanoseconds) since the Unix epoch (1970-01-01T00:00:00Z). It has no time zone and no human-readable fields. Think of it as the machine's view of time: perfect for timestamps, measuring elapsed time, and interoperating with legacy APIs.
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
// Creating Instant instances
Instant now = Instant.now(); // current UTC timestamp
Instant fromEpoch = Instant.ofEpochSecond(1_700_000_000L); // 2023-11-14T22:13:20Z
Instant fromMillis = Instant.ofEpochMilli(1_700_000_000_000L);
Instant parsed = Instant.parse("2024-03-15T10:30:00Z"); // must include Z or offset
// Epoch values
long epochSec = now.getEpochSecond(); // seconds since 1970-01-01T00:00:00Z
long epochMilli = now.toEpochMilli(); // milliseconds since epoch
// Arithmetic
Instant fiveMinutesLater = now.plusSeconds(300);
Instant yesterday = now.minus(java.time.Duration.ofDays(1));
// Converting Instant to human-readable ZonedDateTime
ZonedDateTime zdt = now.atZone(ZoneId.of("America/New_York"));
// Interop with legacy java.util.Date
java.util.Date legacyDate = java.util.Date.from(now); // Instant -> Date
Instant backToInstant = legacyDate.toInstant(); // Date -> Instant
Comparing and measuring with Instant:
import java.time.Instant;
import java.time.Duration;
// Measuring elapsed time — ideal for benchmarking
Instant start = Instant.now();
// ... some operation ...
Instant end = Instant.now();
Duration elapsed = Duration.between(start, end);
System.out.println("Took " + elapsed.toMillis() + " ms");
// Comparisons
Instant a = Instant.parse("2024-01-01T00:00:00Z");
Instant b = Instant.parse("2024-06-15T12:00:00Z");
boolean aIsFirst = a.isBefore(b); // true
boolean same = a.equals(b); // false
// Constants
Instant epoch = Instant.EPOCH; // 1970-01-01T00:00:00Z
Instant max = Instant.MAX; // +1000000000-12-31T23:59:59.999999999Z
Instant is the bridge between java.time and the legacy Date API. Whenever you receive a java.util.Date from an older library, call date.toInstant() first, then convert to whatever java.time type you need.
Duration measures time-based amounts (hours, minutes, seconds, nanoseconds) while Period measures date-based amounts (years, months, days). The distinction matters: adding a Period of one month to January 31 yields February 28/29, while a Duration always adds an exact number of seconds.
import java.time.*;
import java.time.temporal.ChronoUnit;
// Duration — time-based (hours, minutes, seconds)
Duration twoHours = Duration.ofHours(2);
Duration thirtyMin = Duration.ofMinutes(30);
Duration fiveSeconds = Duration.ofSeconds(5);
Duration complex = Duration.parse("PT2H30M"); // ISO-8601: 2 hours 30 minutes
// Duration between two instants or local times
LocalTime start = LocalTime.of(9, 0);
LocalTime end = LocalTime.of(17, 30);
Duration workDay = Duration.between(start, end); // PT8H30M
long totalMinutes = workDay.toMinutes(); // 510
// Period — date-based (years, months, days)
Period oneMonth = Period.ofMonths(1);
Period twoWeeks = Period.ofWeeks(2); // stored as 14 days
Period complex2 = Period.of(1, 6, 15); // 1 year, 6 months, 15 days
Period parsed = Period.parse("P1Y6M15D"); // ISO-8601
// Period between two dates
LocalDate hire = LocalDate.of(2020, 1, 15);
LocalDate now = LocalDate.of(2024, 11, 20);
Period tenure = Period.between(hire, now);
// 4 years, 10 months, 5 days
System.out.println(tenure.getYears()); // 4
System.out.println(tenure.getMonths()); // 10
System.out.println(tenure.getDays()); // 5
// ChronoUnit.between() for a single-unit count
long daysBetween = ChronoUnit.DAYS.between(hire, now); // 1771
long monthsBetween = ChronoUnit.MONTHS.between(hire, now); // 58
// Using Duration and Period
LocalDateTime meeting = LocalDateTime.now().plus(Duration.ofHours(2));
LocalDate renewal = LocalDate.now().plus(Period.ofYears(1));
Period.between() returns years, months, and days as separate fields — they do not collapse. To get the total number of days between two dates, use ChronoUnit.DAYS.between() instead. Similarly, Duration.between() returns a total seconds value, not separate hour/minute/second fields.
DateTimeFormatter handles both formatting (date to string) and parsing (string to date). Unlike the legacy SimpleDateFormat, it is immutable and thread-safe — you can safely declare formatters as static final constants and share them across threads.
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
LocalDateTime now = LocalDateTime.of(2024, 11, 20, 14, 30, 0);
// Predefined ISO formatters
String iso = now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
// 2024-11-20T14:30:00
// Custom patterns
DateTimeFormatter custom = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
String formatted = now.format(custom); // 2024-11-20 14:30
DateTimeFormatter usStyle = DateTimeFormatter.ofPattern("MM/dd/yyyy hh:mm a");
String usFormatted = now.format(usStyle); // 11/20/2024 02:30 PM
// Localized formatters
DateTimeFormatter german = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)
.withLocale(Locale.GERMANY);
String germanDate = LocalDate.of(2024, 11, 20).format(german);
// 20. November 2024
// Parsing strings to dates
LocalDate parsed = LocalDate.parse("2024-03-15"); // uses ISO_LOCAL_DATE by default
LocalDate custom2 = LocalDate.parse("03/15/2024",
DateTimeFormatter.ofPattern("MM/dd/yyyy"));
LocalDateTime parsedDT = LocalDateTime.parse("2024-11-20 14:30",
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
Common format pattern letters:
| Symbol | Meaning | Example |
| yyyy | 4-digit year | 2024 |
| yy | 2-digit year | 24 |
| MM | Month (01-12) | 03 |
| MMM | Month abbreviation | Mar |
| MMMM | Full month name | March |
| dd | Day of month (01-31) | 15 |
| EEE | Day of week abbreviation | Fri |
| EEEE | Full day of week | Friday |
| HH | Hour 24h (00-23) | 14 |
| hh | Hour 12h (01-12) | 02 |
| mm | Minute (00-59) | 30 |
| ss | Second (00-59) | 45 |
| SSS | Millisecond (000-999) | 123 |
| a | AM/PM marker | PM |
| z | Time zone name | EST |
| Z | Zone offset | -0500 |
| VV | Time zone ID | America/New_York |
DateTimeFormatter is immutable and thread-safe. Declare your formatters as static final fields and reuse them throughout your application — there is no need to create a new instance each time, unlike SimpleDateFormat which required per-thread instances or synchronization.
TemporalAdjusters provides reusable strategies for common date manipulations — finding the next Monday, the last day of the month, or the first Friday in a month. They plug into the with() method of any date class.
import java.time.LocalDate;
import java.time.DayOfWeek;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.Temporal;
LocalDate date = LocalDate.of(2024, 11, 20); // a Wednesday
// Built-in adjusters
LocalDate firstDay = date.with(TemporalAdjusters.firstDayOfMonth()); // 2024-11-01
LocalDate lastDay = date.with(TemporalAdjusters.lastDayOfMonth()); // 2024-11-30
LocalDate firstOfYear = date.with(TemporalAdjusters.firstDayOfYear()); // 2024-01-01
LocalDate nextMonth = date.with(TemporalAdjusters.firstDayOfNextMonth()); // 2024-12-01
// Day-of-week adjusters
LocalDate nextMon = date.with(TemporalAdjusters.next(DayOfWeek.MONDAY)); // 2024-11-25
LocalDate prevFri = date.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY)); // 2024-11-15
LocalDate nextOrSame = date.with(TemporalAdjusters.nextOrSame(DayOfWeek.WEDNESDAY)); // 2024-11-20
// Ordinal day-of-week in month
LocalDate firstFri = date.with(TemporalAdjusters.firstInMonth(DayOfWeek.FRIDAY)); // 2024-11-01
LocalDate thirdMon = date.with(TemporalAdjusters.dayOfWeekInMonth(3, DayOfWeek.MONDAY)); // 2024-11-18
// Custom TemporalAdjuster: next working day
TemporalAdjuster nextWorkingDay = temporal -> {
LocalDate d = LocalDate.from(temporal);
DayOfWeek dow = d.getDayOfWeek();
if (dow == DayOfWeek.FRIDAY) return d.plusDays(3);
if (dow == DayOfWeek.SATURDAY) return d.plusDays(2);
return d.plusDays(1);
};
LocalDate workDay = date.with(nextWorkingDay); // 2024-11-21 (Thursday)
// Chaining adjusters for complex logic
// Last working day of the month
LocalDate lastWorkingDay = date
.with(TemporalAdjusters.lastDayOfMonth())
.with(temporal -> {
LocalDate d = LocalDate.from(temporal);
DayOfWeek day = d.getDayOfWeek();
if (day == DayOfWeek.SATURDAY) return d.minusDays(1);
if (day == DayOfWeek.SUNDAY) return d.minusDays(2);
return d;
});
// 2024-11-29 (Friday, since Nov 30 is Saturday)
Custom TemporalAdjuster implementations are a clean way to encapsulate business date logic (payment dates, settlement dates, next business day). Store them as static final fields or in a utility class so they can be reused and tested independently.
Most real-world projects have legacy code, JDBC drivers, or libraries that still use java.util.Date, Calendar, or java.sql types. Java 8 added bridge methods to make conversions straightforward.
Conversion cheat sheet:
| From (Legacy) | To (java.time) | Code |
| java.util.Date |
Instant |
date.toInstant() |
| Instant |
java.util.Date |
Date.from(instant) |
| java.util.Calendar |
ZonedDateTime |
cal.toInstant().atZone(cal.getTimeZone().toZoneId()) |
| ZonedDateTime |
java.util.Calendar |
GregorianCalendar.from(zdt) |
| java.sql.Date |
LocalDate |
sqlDate.toLocalDate() |
| LocalDate |
java.sql.Date |
java.sql.Date.valueOf(localDate) |
| java.sql.Timestamp |
LocalDateTime |
timestamp.toLocalDateTime() |
| LocalDateTime |
java.sql.Timestamp |
Timestamp.valueOf(localDateTime) |
import java.time.*;
import java.util.Date;
import java.util.Calendar;
import java.util.GregorianCalendar;
// java.util.Date <-> Instant
Date legacyDate = new Date();
Instant instant = legacyDate.toInstant();
Date backToDate = Date.from(instant);
// Calendar <-> ZonedDateTime
Calendar cal = Calendar.getInstance();
ZonedDateTime zdt = cal.toInstant().atZone(cal.getTimeZone().toZoneId());
GregorianCalendar gcal = GregorianCalendar.from(zdt);
// java.sql.Date <-> LocalDate
java.sql.Date sqlDate = java.sql.Date.valueOf(LocalDate.now());
LocalDate localDate = sqlDate.toLocalDate();
// java.sql.Timestamp <-> LocalDateTime
java.sql.Timestamp ts = java.sql.Timestamp.valueOf(LocalDateTime.now());
LocalDateTime ldt = ts.toLocalDateTime();
JPA / Hibernate: Since Hibernate 5.x and JPA 2.2, java.time types are supported natively — no converters needed. Map your entity fields directly:
@Entity
public class Event {
@Column(name = "event_date")
private LocalDate eventDate; // maps to DATE column
@Column(name = "start_time")
private LocalTime startTime; // maps to TIME column
@Column(name = "created_at")
private LocalDateTime createdAt; // maps to TIMESTAMP column
@Column(name = "scheduled_at")
private OffsetDateTime scheduledAt; // maps to TIMESTAMP WITH TIME ZONE
@Column(name = "duration")
private Duration duration; // maps to BIGINT (nanoseconds) in Hibernate 6+
}
Jackson serialization: Register the JavaTimeModule to serialize java.time types as ISO-8601 strings instead of arrays:
// Add dependency: com.fasterxml.jackson.datatype:jackson-datatype-jsr310
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// Now LocalDate serializes as "2024-11-20" instead of [2024, 11, 20]
Quick decision guide — which type should I use?
- Do you need only a date (no time)? Use LocalDate.
- Do you need only a time of day (no date)? Use LocalTime.
- Do you need date + time, but all users are in the same timezone? Use LocalDateTime.
- Do you need to schedule across time zones and handle DST? Use ZonedDateTime.
- Are you storing a timestamp in a database or sending it over a REST API? Use OffsetDateTime or Instant.
- Do you need a machine-level timestamp for logging, measuring, or epoch math? Use Instant.
- Do you need to express an amount of time (elapsed, timeout)? Use Duration.
- Do you need to express a calendar amount (age, subscription length)? Use Period.
Best practices summary:
| Scenario | Use | Why |
| Birthday, holiday, contract date | LocalDate | No time or zone needed |
| Store opening hours, alarm time | LocalTime | No date or zone needed |
| Local event (same timezone) | LocalDateTime | No zone ambiguity |
| Meeting across time zones | ZonedDateTime | DST-aware scheduling |
| REST API / database storage | OffsetDateTime | Fixed offset, no DST rules |
| Machine timestamp, elapsed time | Instant | Epoch-based, zone-free |
| Time elapsed (hours/seconds) | Duration | Exact time-based amount |
| Calendar periods (months/years) | Period | Date-based, handles variable month lengths |
When storing timestamps in a database, always use TIMESTAMP WITH TIME ZONE (or OffsetDateTime in Java) rather than TIMESTAMP without a zone. A zoneless timestamp loses offset information and becomes ambiguous when read by applications in different time zones or after a server migration.