Set time to 23:59:59, but database stores next day 00:00:00 — What’s going on?!
1. Problem Description
Yesterday after work, our operations team reported an issue:
Even though the system was configured to lift a blacklist after two days, the prompt was showing three days instead.
Example
- Today: June 16 — user is blacklisted.
- Expected: “You can sign up again on June 19.”
- Actual: June 20.
On investigation, we found the unblocking time had been increased by 1 second:
- Expected: `2025-06-18 23:59:59`
- Stored: `2025-06-19 00:00:00`
In the database:
- About half of the records were correct.
- The rest defaulted to midnight of the next day.
---
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 field type: Date, DB type: timestamp
entity.setDeblockTime(Date.from(resultTime.atZone(ZoneId.systemDefault()).toInstant()));
blackAccountService.save(entity);Potential issue lies in the above snippet.
---
2. Resolution Process
Investigation Steps
Step 1 — Eliminate Code Conflicts
- Verified only one place in the project sets `DeblockTime`.
- Ruled out overwrites from other code.
Step 2 — AI Consultation
Tested code fragment:
LocalDateTime futureTime = currentTime.plus(oaConfigActivityBlacklist.getBlockDay(), ChronoUnit.DAYS);
LocalDateTime resultTime = futureTime.withHour(23).withMinute(59).withSecond(59);
Date date = Date.from(resultTime.atZone(ZoneId.systemDefault()).toInstant());Q:
> Will `Date` always have `23:59:59`?
---
AI Answer — Key Risks
① Daylight Saving Time (DST)
- Scenario: If the target date falls on DST adjustment in system timezone, the result may shift.
- Example: In `Europe/Paris` on Oct 29, 2023, clocks are moved back at 3 AM → shifts may push into the next day.
Using `Date` with system default time zone can lead to:
- Slips by one second
- Roll-over into midnight next day
Best Practices:
- Use UTC consistently.
- Explicitly define time zones.
- Validate timestamp before saving.
---
② Time Zone Conversion Issue
- If `currentTime` is UTC and the system time zone is Asia/Shanghai (UTC+8):
- The conversion may offset final values.
Example:
- UTC: `2023-10-01T16:00:00Z`
- Add 1 day → `2023-10-02T16:00:00Z`
- Set to `23:59:59` → UTC becomes `2023-10-02T15:59:59Z`
---
Step 3 — Batch Insert Test
Reproduction code:
for (int i = 0; i < 100; i++) {
Thread.sleep(100);
LocalDateTime currentTime = LocalDateTime.now();
LocalDateTime futureTime = currentTime.plus(2, ChronoUnit.DAYS);
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);
}Result:
- Half records: `2025-06-19 23:59:59`
- Half records: `2025-06-20 00:00:00`
---
3. Problem Identification
Root cause:
- Java `Date` → millisecond precision
- PostgreSQL `timestamp` → default second precision
- Rounding up occurs when milliseconds ≥ 500, pushing to the next day midnight.
---
4. Solution Proposals
- Truncate milliseconds before inserting:
- Use database higher-precision `timestamp`.
futureTime.withHour(23).withMinute(59).withSecond(59).withNano(0);- Store time in UTC and convert only when displaying.
- Explicitly set `ChronoUnit.SECONDS` before persistence.
---
Adjusting Code Precision
Before:
futureTime.withHour(23).withMinute(59).withSecond(59);After:
futureTime.withHour(23).withMinute(59).withSecond(59).withNano(0);---
Adjusting Database Precision
Before:

After:

---
5. Knowledge Expansion
Java Date vs. LocalDateTime
| Feature | java.util.Date | java.time.LocalDateTime |
|------------------|-----------------------------|--------------------------------------|
| Precision | Milliseconds | Nanoseconds |
| Mutability | Mutable | Immutable |
| Time Zone | Internal UTC, no zone info | No zone info |
---
MySQL `timestamp` vs `datetime`
| Feature | timestamp | datetime |
|------------------|--------------------------------------------------|--------------------------------------|
| Precision | Dependent on definition | Dependent on definition |
| Time Zone | Stored as UTC, converted on retrieval | Stored as literal |
| Range | 1970–2038 | 1000–9999 |
---
Storage Ranges
- DATETIME: `1000-01-01` → `9999-12-31`
- TIMESTAMP: `1970-01-01 UTC` → `2038-01-19 UTC`
Precision
- Both support microseconds with `(6)` definition from MySQL 5.6.4
Storage Size
- DATETIME: 8 bytes
- TIMESTAMP: 4 bytes
---
Recommended Use Cases
| Scenario | Recommended Type | Reason |
|----------------------------------------|--------------------|------------------------------------------|
| Historical event (order creation) | DATETIME | Not time zone dependent |
| Server local time logs | DATETIME | Reflects server time directly |
| Multi-time-zone app (cross-border) | TIMESTAMP | Automatic time zone conversion |
| Auto timestamp updates | TIMESTAMP | Supports `ON UPDATE CURRENT_TIMESTAMP` |
---
6. Summary
We debugged an issue where stored timestamps often ended 1 second too many:
- Caused by mismatch between Java `Date` precision and DB `timestamp` precision.
- PostgreSQL rounded up seconds when milliseconds ≥ 500.
Fix Options:
- Truncate Java nanoseconds before persisting.
- Increase DB precision to match Java’s.
Also covered:
- Differences between Java and DB time types.
- Where each type is best applied.
Time consistency is critical for any cross-system or multi-platform workload — including global publication scheduling.