LocalDateTime is the right type when one value has to carry both a date and a time of day: an order placed at 2:30 PM on May 14th, a daily report cutoff at midnight, a price drop scheduled for next Friday at 9 AM. This lesson covers how to build a LocalDateTime, how to slice it back into its date and time parts, how to do arithmetic that crosses day boundaries, and one important caveat: a LocalDateTime has no timezone, so the same value means different absolute moments in different places on Earth.
LocalDateTime IsA LocalDateTime is a single immutable value that stores a year, month, day, hour, minute, second, and nanosecond. That's it. There's no timezone attached, no offset from UTC, no daylight-savings awareness. The string "2026-12-25 at 09:00" represents a LocalDateTime. It says when the event happens given the right local position, but it doesn't specify which location.
That's a feature, not a bug, for the cases it's meant for. The schedule for a brick-and-mortar store opening at 09:00 on Christmas morning is the same LocalDateTime regardless of whether the chain has stores in Mumbai, London, or San Francisco. Each store opens at 09:00 its time. A single absolute moment shared across all of them is not the goal; the same wall-clock value is.
The T between the date and time is part of the ISO-8601 format Java uses by default. The parsing section below covers the format. For now, the printed value has no zone, no +05:30, no Z. That absence is the point.
A LocalDateTime is built on top of two pieces. The date portion is a LocalDate, and the time portion is a LocalTime. The composition diagram makes the relationship explicit:
The diagram shows what's happening conceptually: a LocalDateTime is the pair (LocalDate, LocalTime) packaged into one value. A LocalDateTime can be decomposed back into those two pieces, and built from them when the input arrives that way. Both directions are shown below.
LocalDateTimeLike its siblings LocalDate and LocalTime, LocalDateTime has no public constructor. Instances come from static factory methods. There are four shapes to be aware of.
now() for the Current MomentLocalDateTime.now() reads the system clock and returns the current date and time, using the JVM's default timezone to decide what "now" looks like on a wall clock. Use this for a quick timestamp in a log line, a cutoff check, or a "when did this run" record.
The exact value depends on when the code runs. Note that now() already factored in the default timezone to pick a wall-clock value, and then threw the timezone information away. A server in Asia/Kolkata and a server in America/Los_Angeles will return different LocalDateTime.now() values at the same physical instant. That's a reminder that LocalDateTime is about wall-clock values, not absolute moments.
of(...) for an Exact ValueLocalDateTime.of(...) is overloaded into several flavors. The most common form takes year, month, day, hour, minute, and optionally second and nanosecond:
The factory validates the values, so LocalDateTime.of(2026, 2, 30, 9, 0) throws DateTimeException because February doesn't have 30 days, and LocalDateTime.of(2026, 5, 14, 25, 0) throws because hour 25 doesn't exist. That validation is one of the reasons the modern API is safer than the old java.util.Date, which silently accepted out-of-range values and rolled them over.
The month can also be passed as the Month enum instead of an int, which makes calls easier to read at the call site:
Month.JUNE is unambiguous; 6 requires a reader to stop and count. Use the enum when the call site is far from where the integer constants are obvious.
LocalDate and a LocalTimeWhen the date and time arrive separately (a date picker plus a time picker in a UI, two columns in a database row), building them as separate values and joining them at the end is natural. There are two equivalent ways:
All three produce the same value. Pick whichever reads best at the call site. saleDate.atTime(saleTime) reads well when the date is the dominant concept ("the sale day, at this time"); saleTime.atDate(saleDate) reads well when the time is the focus ("9 AM, on this date"). LocalDateTime.of(date, time) is the neutral one.
parse(...) for ISO-8601 TextLocalDateTime.parse(text) reads a string in the default ISO-8601 format and returns a LocalDateTime. The format is yyyy-MM-ddTHH:mm with optional seconds and fractional seconds:
The T separator is required, the date must be zero-padded (2026-05-14, not 2026-5-14), and trailing components are optional. Any deviation throws DateTimeParseException. For custom formats like 14/05/2026 02:30 PM, a DateTimeFormatter is required.
LocalDateTimeA LocalDateTime exposes the date and time parts at any time. toLocalDate() returns the LocalDate portion; toLocalTime() returns the LocalTime portion. No information is lost, because that's all a LocalDateTime is made of in the first place.
Beyond the two big getters, the individual field getters from LocalDate and LocalTime are available directly on LocalDateTime, with no need to go through the split first:
getMonth() returns the Month enum (MAY), while getMonthValue() returns the integer 5. Use whichever matches the downstream code: enums for switch statements, integers for arithmetic or external formats.
The reason LocalDateTime exists, instead of juggling a LocalDate and a LocalTime by hand, is that arithmetic across the date/time boundary just works. Adding 12 hours to 8 PM gives 8 AM the next day, with the date rolled over automatically.
LocalDateTime has plusXxx and minusXxx methods for every reasonable unit:
| Method | Adds |
|---|---|
plusYears(n) | n years |
plusMonths(n) | n months |
plusWeeks(n) | n weeks (7 days each) |
plusDays(n) | n days |
plusHours(n) | n hours |
plusMinutes(n) | n minutes |
plusSeconds(n) | n seconds |
plusNanos(n) | n nanoseconds |
Each has a corresponding minusXxx. Each returns a new LocalDateTime because the class is immutable; the receiver doesn't change.
The interesting case is the boundary cross. Add hours that push past midnight and the date moves forward automatically:
Thirty minutes after 23:45 on the 14th is 00:15 on the 15th. No manual check for crossing midnight is needed; the API handles it. The same is true going backward (subtracting hours into the previous day), across month boundaries, and across year boundaries.
Every plusXxx/minusXxx allocates a new LocalDateTime because the class is immutable. That's negligible for a handful of calls in a request handler. For date arithmetic in a tight loop over millions of records, profile before assuming the allocations are free, and consider working with epoch seconds via Instant if there's a measured cost.
Month arithmetic has one detail to remember: adding a month to a date that doesn't exist in the target month clamps to the last valid day. Adding one month to 2026-01-31T10:00 gives 2026-02-28T10:00, not an error and not 2026-03-03. Lesson 2 covered this in more depth for LocalDate; the same rule applies here.
withXxxplusXxx and minusXxx are for relative changes. To set a specific field to a specific value, use the withXxx family. Each method returns a copy of the original with one field replaced:
| Method | Replaces |
|---|---|
withYear(y) | Year |
withMonth(m) | Month (1-12) |
withDayOfMonth(d) | Day of month |
withHour(h) | Hour (0-23) |
withMinute(m) | Minute (0-59) |
withSecond(s) | Second (0-59) |
withNano(n) | Nanosecond (0-999,999,999) |
A common e-commerce use is "snap an order timestamp to the cutoff time for daily reporting":
The chaining works because each withXxx returns a new LocalDateTime, so the next call operates on that intermediate value. The receiver placed is unchanged at the end of all four calls.
withXxx validates the same way of does. placed.withDayOfMonth(31) on a value whose month is February throws DateTimeException, because February doesn't have a day 31. For "last day of the month" regardless of which month, use TemporalAdjusters.lastDayOfMonth().
Comparing LocalDateTime values with == rarely does what's intended. Two equal-valued instances might or might not be the same object reference, and the usual question is "is this moment before that one," not "is this the same instance." Use isBefore, isAfter, isEqual, or compareTo.
The expression !now.isBefore(saleStart) means "now is at-or-after saleStart". isBefore is strict (<), so its negation is >=. The same pattern with !now.isAfter(saleEnd) gives <=. Combined, this produces a half-open window [start, end] that includes both endpoints.
compareTo returns a negative number, zero, or a positive number, the same as Comparable everywhere else in Java. Use it for sorting:
Collections.sort uses compareTo internally, which sorts LocalDateTime values chronologically. No custom comparator needed.
isEqual and equals give the same answer for LocalDateTime (unlike ChronoLocalDateTime, where chronology can differ). For straightforward LocalDateTime comparisons, either works.
isBefore and isAfter compare each field in order (year, month, day, hour, minute, second, nano) until they find a difference. That's O(1) and far cheaper than parsing back to a string or converting to milliseconds.
Given two LocalDateTime values, the distance between them is a common need. The ChronoUnit enum exposes between(start, end) for any unit:
ChronoUnit.DAYS.between returns whole units only and rounds toward zero. Three days and nine and a half hours is 3 days, not 3.5 and not 4. Swapping the arguments to put the later value first makes the result negative; the call doesn't validate ordering, which is occasionally useful and occasionally a bug.
Lesson 7 covers Duration and Period properly. ChronoUnit.between is the lightweight tool for getting just a number.
A common source of confusion. Two systems each store the value 2026-12-25T09:00 and call it the moment of a flash sale. They aren't actually agreeing on the same instant in time.
If the Mumbai server interprets 2026-12-25T09:00 in Asia/Kolkata and the New York server interprets the same string in America/New_York, the two instants are 10.5 hours apart. The Mumbai sale starts at 2026-12-24T22:30 UTC; the New York sale starts at 2026-12-25T14:00 UTC. Same LocalDateTime, different absolute moment.
That's not a bug in LocalDateTime. It's the contract. A LocalDateTime is deliberately zone-free because some use cases require it:
For everything else, pairing it with a zone is required. Lesson 5 covers ZonedDateTime, which is a LocalDateTime plus a ZoneId, and lesson 6 covers Instant, the unambiguous "this many nanoseconds since 1970 UTC" representation. The decision tree:
The first question: "does this need to mean the same instant for everyone?" If yes, ZonedDateTime or Instant is the right type. Instant fits when only comparison, sorting, or storage of the moment matters, and the wall-clock representation is irrelevant. ZonedDateTime fits when both a stable instant and rendering in a particular timezone are required (UI display, scheduled events whose local hour matters).
If the answer is no, that the value is tied to a wall clock that travels with the user or store, LocalDateTime is exactly the type.
A practical rule: anything written to a shared database or sent over an API as "this is when it happened" should rarely be a LocalDateTime. Use Instant for that. Use LocalDateTime for inputs from a form (the user picked a date and time in their local interpretation), for schedules that recur at the same wall-clock time in each store, and for daily-report cutoffs whose meaning is "midnight, wherever this report runs."
InstantA LocalDateTime sometimes needs to become an absolute instant, for example before storing in a column that holds UTC timestamps. The conversion requires supplying the missing piece: which timezone the LocalDateTime is in.
The same LocalDateTime becomes two different Instant values depending on the paired zone, which is the same problem from earlier shown in concrete form. ZonedDateTime appears here only to make the conversion path work; lesson 5 covers it in depth.
Going the other way, an Instant plus a zone returns a LocalDateTime:
The same absolute instant projects to two different wall clocks. That's the key relationship: an Instant is a single point on the global timeline, and a LocalDateTime is what that point looks like when read off a clock somewhere on Earth.
Calling toString() on a LocalDateTime returns the ISO-8601 representation. The same format is what parse(text) reads. The shape is:
Trailing components are omitted when they're zero. LocalDateTime.of(2026, 5, 14, 14, 30, 0) prints as 2026-05-14T14:30, not 2026-05-14T14:30:00, because the seconds are zero. Add a non-zero second and they appear: 2026-05-14T14:30:42. Fractional seconds (up to 9 digits, since LocalDateTime supports nanoseconds) print only when nonzero.
ISO-8601 is appropriate for logs, JSON payloads, and any text format that needs to round-trip cleanly. It sorts lexically in chronological order (because the components are in big-endian order, year first), and it parses without ambiguity. For human-facing strings like May 14, 2026 at 2:30 PM, a DateTimeFormatter is required.
A common bug with LocalDateTime is forgetting that every modifying call returns a new value instead of changing the original. The class is immutable, so methods like plusDays, withHour, and minusMinutes cannot change the receiver.
What's wrong with this code?
The output is Arrives by: 2026-05-14T14:30. The plusDays(3) call computed a new LocalDateTime three days later, but nothing captured it, so the JVM threw the result away. placed still points to the original value.
Fix:
Capture the return value. Either into a new variable, or by reassigning the original (placed = placed.plusDays(3)). The pattern is the same for every method in java.time: the call doesn't mutate, it computes.
10 quizzes