skip to content

Immutability & Fluent API of java.time

Every java.time type is immutable and thread-safe, so plusDays and withX return a new value instead of mutating. Discarding that return value is a real and easily-demonstrated bug, which is why interviewers mention it.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

4

Why does `date.plusDays(1);` on its own line have no effect, and how do you fix it?

level: juniorimportance: must knowfreq 85%

answer

  1. Methods return a new object, never mutate
  2. date.plusDays(1); with no '=' is a no-op
  3. Reassign: date = date.plusDays(1)
  4. Treat like String: s.toUpperCase() alone is useless
  5. Calendar.add mutated; java.time does not

basics

~20 s

All java.time types are immutable, so plusDays builds and returns a NEW date instead of changing the old one. If you ignore the returned value, nothing changes. Fix it by assigning the result: date = date.plusDays(1).

solid answer

~40 s

Every java.time type (LocalDate, LocalDateTime, Instant, Duration, etc.) is immutable: methods like plusDays, minusHours, and withYear never mutate the receiver. They compute a new instance and return it. So a statement like date.plusDays(1); creates a new LocalDate and immediately throws it away because the return value is unused; the original date variable still points at the old value. This is the single most common java.time bug, and it compiles cleanly because the method genuinely returns a value you are free to ignore. The fix is to capture the result, usually by reassigning: date = date.plusDays(1); or by chaining and assigning the final result. The same rule applies to all the plus/minus/with families and to Duration/Period arithmetic.

go deeper

for a junior

Knows java.time objects don't change in place and that you must reassign the result of plusDays/minusX.

for a middle

Explains the immutable return-new-instance pattern, why it compiles silently, and contrasts it with legacy Calendar.add mutation.

for a senior

Connects immutability to thread-safety/safe-sharing trade-offs and points to lint tooling (Error Prone CheckReturnValue, IDE inspections) that catches the discarded-result bug in CI/review.

for a principal

Frames immutability as a deliberate value-type design decision (JSR-310), discusses defensive-copy elimination and how to enforce no-ignored-return as a team-wide static-analysis rule.

## The setup `java.time` is the modern date-and-time API introduced in Java 8 (JSR-310), living in the `java.time` package: `LocalDate` (a date with no time), `LocalTime`, `LocalDateTime`, `Instant` (a point on the timeline in UTC), `ZonedDateTime`, plus the amounts `Duration` (seconds/nanos) and `Period` (years/months/days). ## What 'immutable' means An object is **immutable** when, once created, its internal state can never change. There is no setter, no method that alters a field. Every `java.time` type is immutable. So a method whose name suggests a change — `plusDays`, `minusHours`, `withYear`, `truncatedTo` — cannot actually change the object you call it on. Instead it follows a pattern: read this object's fields, compute the new values, **construct and return a brand-new object**, and leave the original untouched. ## Why `date.plusDays(1);` does nothing Consider: ```java LocalDate date = LocalDate.of(2026, 1, 1); date.plusDays(1); // <-- bug System.out.println(date); // prints 2026-01-01, NOT 2026-01-02 ``` The call `date.plusDays(1)` does run. It builds a new `LocalDate` representing 2026-01-02 and returns it. But the statement does nothing with the return value — there is no `=`, no further call — so the new object is immediately eligible for garbage collection. The variable `date` was never reassigned; it still references the original 2026-01-01 object (which could not have changed because the type is immutable). This compiles without warning because a method returning a value you choose to ignore is perfectly legal Java. The compiler does not know you *intended* to use it. (This is exactly the trap people coming from `java.util.Date`/`Calendar` fall into, because `Calendar.add(...)` *did* mutate in place.) ## The fix Capture the returned value: ```java date = date.plusDays(1); // reassign ``` or, when chaining, assign the final result of the chain: ```java LocalDate due = date.plusMonths(1).plusDays(15); ``` The receiver `date` is still unchanged; `due` holds the new value. ## Why immutability was chosen 1. **Thread safety** — an immutable object can be shared across threads with zero synchronization, because no thread can change it. 2. **Safe sharing / no defensive copies** — you can hand a `LocalDate` to a caller without fearing they mutate your copy. 3. **Predictability** — a value can't change underneath you between two lines of code. The trade-off is exactly this gotcha: you must always *use the return value*. A useful habit is to treat `java.time` types like `String` (also immutable): `s.toUpperCase();` alone is equally useless. ## Spotting it in review Any line that is *just* `x.plusXxx(...)` / `x.minusXxx(...)` / `x.withXxx(...)` / `x.truncatedTo(...)` with no assignment and no further use is almost certainly a bug. Some linters (e.g. Error Prone's `CheckReturnValue`, IntelliJ inspections) flag ignored return values of `@CheckReturnValue`-annotated or known-pure methods.

  • Does the same discarded-result trap apply to Duration and Period?
    Yes. Duration and Period are immutable too, so duration.plusMinutes(5); with no assignment is also a no-op; you must capture the returned amount.
  • Why does this code still compile without any warning?
    Ignoring a method's return value is legal Java; the compiler has no idea you intended to use it. Only external tools/inspections (Error Prone @CheckReturnValue, IDE inspections) flag it.

saying these in an interview costs you the question

  • Believing plusDays/minusX/withX mutate the receiver
  • Thinking the compiler will warn you about the discarded result
  • Confusing java.time behavior with legacy Calendar.add()
  • Saying you must call a 'commit' or 'apply' method afterward

context

open as a page

How does the fluent API of java.time let you build a derived date/time, and what makes chaining safe?

level: middleimportance: should knowfreq 60%

basics

~10 s

Each plus/minus/with method returns a new java.time object, so you can chain calls like date.plusMonths(1).withDayOfMonth(1). Every step returns a fresh value, and the original is never touched, so chaining is safe.

open as a page

Why are java.time types safe to share across threads and cache as constants, unlike java.util.Date and SimpleDateFormat?

level: seniorimportance: should knowfreq 55%

basics

~10 s

java.time types never change after creation (immutable), so multiple threads reading the same instance can't interfere. The old Date is mutable and SimpleDateFormat keeps internal state, so sharing them across threads causes corruption.

open as a page

Given java.time creates a new object on every plus/minus/with call, when (if ever) is this allocation a real concern, and how would you reason about it?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

Each call makes a small new object, but these are tiny and short-lived, so the JVM handles them cheaply. It only matters in very hot, high-volume loops; usually you measure first and prefer correctness and clarity over avoiding allocation.

open as a page