Why Adding 2 Days Sometimes Results in a 3-Day Unblock? Java Date Precision Explained

A Java backend bug caused blacklist unblocking times to be stored one second later than expected due to mismatched Date and TIMESTAMP precision, daylight‑saving and timezone conversions, and millisecond rounding, which was diagnosed through code review, AI analysis, and batch testing, then fixed by normalising nanoseconds or adjusting database column precision.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Why Adding 2 Days Sometimes Results in a 3-Day Unblock? Java Date Precision Explained

Problem Description

After work, operations reported that a blacklist set to be lifted after two days was showing a three‑day wait. For example, a user blacklisted on 2025‑06‑16 was told they could re‑register on 2025‑06‑19, but the system displayed 2025‑06‑20.

The expected deblock time was 2025-06-18 23:59:59, but the database stored 2025-06-19 00:00:00. Roughly half of the records were correct, the other half ended at midnight.

Code Logic

LocalDateTime currentTime = LocalDateTime.now();
LocalDateTime futureTime = currentTime.plus(2, ChronoUnit.DAYS);
// set to the last second of the day
LocalDateTime resultTime = futureTime.withHour(23).withMinute(59).withSecond(59);
BlackAccount entity = new BlackAccount();
entity.setDeblockTime(Date.from(resultTime.atZone(ZoneId.systemDefault()).toInstant()));
blackAccountService.save(entity);

The code seemed correct, but the stored value was off by one second.

Investigation Steps

1. Eliminate Code Issues

Confirmed only one place sets DeblockTime; no other code overwrites it.

2. Ask AI

The AI suggested two possible causes:

DST Impact – If the target date falls on a daylight‑saving transition, the conversion to UTC may shift the time by an hour.

Timezone Conversion – If currentTime is in a different zone (e.g., UTC) and later converted to the system zone, rounding can add a second.

Seeing the data distribution, both 23:59:59 and 00:00:00 appear, even when creation times differ by only a few minutes.

3. Batch Insert Test

for (int i = 0; i < 100; i++) {
    Thread.sleep(100);
    LocalDateTime currentTime = LocalDateTime.now();
    LocalDateTime futureTime = currentTime.plus(2, ChronoUnit.DAYS);
    // set to the last second of the day
    LocalDateTime resultTime = futureTime.withHour(23).withMinute(59).withSecond(59);
    BlackAccount entity = new BlackAccount();
    entity.setDeblockTime(Date.from(resultTime.atZone(ZoneId.systemDefault()).toInstant()));
    blackAccountService.save(entity);
}

The test reproduced the issue: half the rows stored 2025-06-19 23:59:59, the other half 2025-06-20 00:00:00.

Root Cause

PostgreSQL TIMESTAMP stores values with second precision. When the Java Date contains milliseconds ≥ 500, the database rounds up to the next second, causing the stored time to jump to midnight of the following day.

Solutions

Clear sub‑second precision in Java before persisting:

futureTime.withHour(23).withMinute(59).withSecond(59).withNano(0);

Adjust the database column to a precision that matches Java’s millisecond precision (e.g., TIMESTAMP(3) or DATETIME with appropriate fractional seconds).

Knowledge Expansion

1. Date vs LocalDateTime

Feature

java.util.Date
java.time.LocalDateTime

Precision

millisecond

nanosecond

Package

java.util.Date
java.time.LocalDateTime

Mutability

mutable

immutable

Time‑zone awareness

stores UTC epoch, no zone info

no zone info, local date‑time only

2. MySQL TIMESTAMP vs DATETIME

Feature

DATETIME
TIMESTAMP

Range

1000‑01‑01 00:00:00 to 9999‑12‑31 23:59:59

1970‑01‑01 00:00:01 UTC to 2038‑01‑19 03:14:07 UTC

Precision

microsecond (DATETIME(6))

microsecond (TIMESTAMP(6))

Storage

8 bytes

4 bytes (UTC)

Time‑zone handling

no zone conversion

auto‑converts to UTC on write, to session zone on read

Default value

none unless set

supports DEFAULT CURRENT_TIMESTAMP and

ON UPDATE CURRENT_TIMESTAMP

3. Recommended Scenarios

Use LocalDateTime for simple local timestamps; prefer ZonedDateTime or OffsetDateTime when time‑zone handling is required.

For JDBC 4.2+, LocalDateTime can be set directly with PreparedStatement#setObject.

Prefer java.time API for thread‑safe, high‑precision date‑time operations.

Conclusion

The mismatch between Java’s millisecond precision and PostgreSQL’s second‑precision TIMESTAMP caused about half of the deblock times to be stored one second later, appearing as a three‑day wait. The issue was resolved by either zeroing sub‑second nanoseconds in Java or aligning the database column’s precision, and the article also compares Java and database time types and their appropriate use cases.

image.png
image.png
image.png
image.png
JavadatetimetimestampTimezonePostgreSQL
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.