Date-time bugs are common in distributed systems:
- wrong timezone assumptions
- DST edge-case failures
- inconsistent serialization formats
- mixing local and global time concepts
Java 8 java.time API solves most of these when modeled correctly.
Core Types and When to Use Them
Instant: machine timestamp in UTC (event time, audit fields)LocalDate: date without timezone (birth date, business date)LocalDateTime: date+time without zone (rare for persisted events)ZonedDateTime: date+time with timezone (UI/business timezone logic)OffsetDateTime: date+time with offset (API payloads)
Rule: persist timeline events as Instant.
Persist and Display Pattern
Persist in UTC:
Instant createdAt = Instant.now();
Display in user timezone:
ZoneId userZone = ZoneId.of("America/Los_Angeles");
String display = ZonedDateTime.ofInstant(createdAt, userZone)
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z"));
This avoids server timezone coupling.
Parsing and Formatting API Timestamps
ISO-8601 input from API:
Instant ts = Instant.parse("2026-03-07T10:15:30Z");
Offset timestamp parsing:
OffsetDateTime odt = OffsetDateTime.parse("2026-03-07T15:45:00+05:30");
Instant normalized = odt.toInstant();
For external integrations, always normalize to Instant internally.
DST Edge Case Example
ZoneId zone = ZoneId.of("America/New_York");
LocalDateTime local = LocalDateTime.of(2026, 3, 8, 2, 30); // DST transition day
ZonedDateTime zoned = local.atZone(zone);
Some local times are invalid or ambiguous during DST transitions.
Use ZonedDateTime and clear business rules for scheduling.
LocalDateTime Persistence Trap
LocalDateTime has no timezone/offset.
Persisting it for global events can create ambiguity across regions.
Bad for event timestamps:
LocalDateTime createdAt = LocalDateTime.now(); // ambiguous globally
Preferred:
Instant createdAt = Instant.now(); // unambiguous timeline point
Use LocalDateTime only when timezone is intentionally irrelevant.
Expiry and Duration Logic
Use Duration or Period instead of manual millis arithmetic.
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(Duration.ofMinutes(15));
boolean expired = Instant.now().isAfter(expiresAt);
API Contract Recommendation
For external JSON APIs, standardize on ISO-8601 UTC strings:
{
"createdAt": "2026-03-07T10:15:30Z"
}
Avoid custom date formats unless integration requires it. If custom format is mandatory, define formatter centrally and version the contract.
Database Mapping Guidance
- prefer DB types that preserve timezone semantics (
TIMESTAMP WITH TIME ZONEwhere supported) - if DB lacks true timezone support, store epoch millis/seconds or UTC instant strings consistently
- never depend on DB/session local timezone for business correctness
Consistency across app + DB + analytics pipelines is more important than local convenience.
Testing Time-Dependent Logic
Inject Clock instead of calling Instant.now() everywhere.
public class TokenService {
private final Clock clock;
public TokenService(Clock clock) {
this.clock = clock;
}
public Instant issuedAt() {
return Instant.now(clock);
}
}
Deterministic test:
Clock fixed = Clock.fixed(Instant.parse("2026-03-07T00:00:00Z"), ZoneOffset.UTC);
Architecture Rules for Distributed Systems
- store timestamps as UTC
Instant - convert to user timezone only at presentation layer
- standardize API format to ISO-8601
- avoid legacy
java.util.Date/Calendarin new code - centralize time utilities and timezone policy