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.
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.LocalDateTimePrecision
millisecond
nanosecond
Package
java.util.Date java.time.LocalDateTimeMutability
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 TIMESTAMPRange
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_TIMESTAMP3. 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.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
