The issue you're seeing is due to the difference in time zone database being used in the two versions of Java. Time zone rules are updated over time, and you've encountered an old date that is treated differently under newer rules.

For the versions of Java on my machine, I'm getting 1934-07-22 for Java 11 & 17 (time zone database versions 2020d & 2021e, respectively), but 1934-07-21 for Java 21 (time zone database version 2023c).

The specific reason for this is that 2020d & 2021e have a zone offset of +01:19:32 for this instant in Europe/Amsterdam. On the other hand, 2023c has a zone offset of +01:00 in Europe/Amsterdam. Adding 1:19:32 to 1934-07-21T22:40:28Z results in midnight the next day (1934-07-22), but adding 1:00:00 results in 23:40:28 the same day (1934-07-21).

Time zone database change

I did some Internet sleuthing, and per the time zone database mailing list, it looks like this change was made in time zone database version 2022b. So 2022a and earlier use the +01:19:32 offset, and 2022b and later use the +01:00 offset.

2022b merged the Amsterdam & Brussels time zones, which are identical after 1970, but differ in sufficiently early historical dates. It looks like Amsterdam at one point used local mean time, hence the strange-looking offset. However, it seems that Brussels did not.

Between 2022a an b the timezone info for Europe/Amstedam is removed:
      Z Europe/Amsterdam 0:19:32 - LMT 1835
The timezone info for Brussels around that date differs from Amsterdams timezone info.

Determining the timezone information for dates around that period on my Linux system in python returns since the latest update an offset of 0 minutes as opposed to the 19 minutes previously.

Is the removal for this timezone information intentional or a bug?

The maintainer has merged the Amsterdam and Brussels timezone information by using the Brussels rules for Amsterdam, as the rules don't differ since 1970, in the default data package. This wipes out Amsterdam rules from before 1970, including local mean time.

Demonstration

String javaVersion = System.getProperty("java.vendor") + Runtime.version();
String timeZoneDatabase = ZoneRulesProvider.getVersions("UTC")
        .keySet().stream().findFirst().orElseThrow();
Date date = new Date(-1118625572000L);
ZoneId timeZone = ZoneId.of("Europe/Amsterdam");
ZonedDateTime zonedDateTime = date.toInstant().atZone(timeZone);
LocalDate localDate = zonedDateTime.toLocalDate();

System.out.printf("Java: %s%n", javaVersion);
System.out.printf("Time Zone Database: %s%n", timeZoneDatabase);
System.out.printf("Instant (UTC): %s%n", date.toInstant());
System.out.printf("LocalDate (Europe/Amsterdam): %s%n", localDate);
System.out.printf("Time zone offset: %s%n", zonedDateTime.getOffset());

Output

Java: AdoptOpenJDK11.0.10+9
Time Zone Database: 2020d
Instant (UTC): 1934-07-21T22:40:28Z
LocalDate (Europe/Amsterdam): 1934-07-22
Time zone offset: +01:19:32
Java: Eclipse Adoptium17.0.2+8
Time Zone Database: 2021e
Instant (UTC): 1934-07-21T22:40:28Z
LocalDate (Europe/Amsterdam): 1934-07-22
Time zone offset: +01:19:32
Java: Eclipse Adoptium21.0.1+12-LTS
Time Zone Database: 2023c
Instant (UTC): 1934-07-21T22:40:28Z
LocalDate (Europe/Amsterdam): 1934-07-21
Time zone offset: +01:00
Answer from M. Justin on Stack Overflow
🌐
Wikipedia
en.wikipedia.org › wiki › Java_version_history
Java version history - Wikipedia
4 weeks ago - The release on December 8, 1998 and subsequent releases through J2SE 5.0 were rebranded retrospectively Java 2 and the version name "J2SE" (Java 2 Platform, Standard Edition) replaced JDK to distinguish the base platform from J2EE (Java 2 Platform, Enterprise Edition) and J2ME (Java 2 Platform, ...
🌐
Oracle
docs.oracle.com › en › java › javase › 17 › docs › api › java.base › java › util › Date.html
Date (Java SE 17 & JDK 17)
January 20, 2026 - A date (day of month) is represented by an integer from 1 to 31 in the usual manner. An hour is represented by an integer from 0 to 23. Thus, the hour from midnight to 1 a.m. is hour 0, and the hour from noon to 1 p.m. is hour 12. A minute is represented by an integer from 0 to 59 in the usual ...
🌐
Oracle
oracle.com › java › technical details › java se
Consolidated JDK 17 Release Notes
The following sections summarize changes made in all Java SE 17.0.13 BPR releases. The BPR releases are listed below in date order, most current BPR first.
🌐
Programming Hints
programminghints.com › home › still using java.util.date? don’t!
Still using java.util.Date? Don't! - Programming Hints
June 23, 2017 - There are many workarounds in the Java world around this banal design decision, like handling years before 1900. Months are zero indexed (0 – January, 11 – December). Not very intuitive and led to many off-by-one errors. All classes in this old API are mutable. As a result, any time you want to give a date back (say, as an instance structure) you need to return a clone of that date instead of the date object itself (since otherwise, people can mutate your structure).
🌐
Baeldung
baeldung.com › home › java › java dates › an introduction to instantsource in java
An Introduction to InstantSource in Java | Baeldung
January 8, 2024 - In this tutorial, we’ll dive into the InstantSource interface introduced in Java 17, which provides a pluggable representation of the current instant and avoids references to time zones.
Top answer
1 of 2
7

The issue you're seeing is due to the difference in time zone database being used in the two versions of Java. Time zone rules are updated over time, and you've encountered an old date that is treated differently under newer rules.

For the versions of Java on my machine, I'm getting 1934-07-22 for Java 11 & 17 (time zone database versions 2020d & 2021e, respectively), but 1934-07-21 for Java 21 (time zone database version 2023c).

The specific reason for this is that 2020d & 2021e have a zone offset of +01:19:32 for this instant in Europe/Amsterdam. On the other hand, 2023c has a zone offset of +01:00 in Europe/Amsterdam. Adding 1:19:32 to 1934-07-21T22:40:28Z results in midnight the next day (1934-07-22), but adding 1:00:00 results in 23:40:28 the same day (1934-07-21).

Time zone database change

I did some Internet sleuthing, and per the time zone database mailing list, it looks like this change was made in time zone database version 2022b. So 2022a and earlier use the +01:19:32 offset, and 2022b and later use the +01:00 offset.

2022b merged the Amsterdam & Brussels time zones, which are identical after 1970, but differ in sufficiently early historical dates. It looks like Amsterdam at one point used local mean time, hence the strange-looking offset. However, it seems that Brussels did not.

Between 2022a an b the timezone info for Europe/Amstedam is removed:
      Z Europe/Amsterdam 0:19:32 - LMT 1835
The timezone info for Brussels around that date differs from Amsterdams timezone info.

Determining the timezone information for dates around that period on my Linux system in python returns since the latest update an offset of 0 minutes as opposed to the 19 minutes previously.

Is the removal for this timezone information intentional or a bug?

The maintainer has merged the Amsterdam and Brussels timezone information by using the Brussels rules for Amsterdam, as the rules don't differ since 1970, in the default data package. This wipes out Amsterdam rules from before 1970, including local mean time.

Demonstration

String javaVersion = System.getProperty("java.vendor") + Runtime.version();
String timeZoneDatabase = ZoneRulesProvider.getVersions("UTC")
        .keySet().stream().findFirst().orElseThrow();
Date date = new Date(-1118625572000L);
ZoneId timeZone = ZoneId.of("Europe/Amsterdam");
ZonedDateTime zonedDateTime = date.toInstant().atZone(timeZone);
LocalDate localDate = zonedDateTime.toLocalDate();

System.out.printf("Java: %s%n", javaVersion);
System.out.printf("Time Zone Database: %s%n", timeZoneDatabase);
System.out.printf("Instant (UTC): %s%n", date.toInstant());
System.out.printf("LocalDate (Europe/Amsterdam): %s%n", localDate);
System.out.printf("Time zone offset: %s%n", zonedDateTime.getOffset());

Output

Java: AdoptOpenJDK11.0.10+9
Time Zone Database: 2020d
Instant (UTC): 1934-07-21T22:40:28Z
LocalDate (Europe/Amsterdam): 1934-07-22
Time zone offset: +01:19:32
Java: Eclipse Adoptium17.0.2+8
Time Zone Database: 2021e
Instant (UTC): 1934-07-21T22:40:28Z
LocalDate (Europe/Amsterdam): 1934-07-22
Time zone offset: +01:19:32
Java: Eclipse Adoptium21.0.1+12-LTS
Time Zone Database: 2023c
Instant (UTC): 1934-07-21T22:40:28Z
LocalDate (Europe/Amsterdam): 1934-07-21
Time zone offset: +01:00
2 of 2
-1

tl;dr

Your issue of differing dates likely has nothing to do with MongoDB, and nothing to do with versions of Java. Your issue is probably due to using different time zones.

The same moment has different dates when seen through different time zones.

Your code:

println("Date: ${date.toInstant()?.atZone(ZoneId.systemDefault())!!.toLocalDate()}")

… uses default time zone. I suspect that default varied at runtime.

Do not do that, do not rely upon such defaults. Specify your expected/desired time zone explicitly. For example:

Instant.ofEpochMilli( x ).atZone( ZoneId.of( "Europe/Paris" ) ).toLocalDate()

Details

We've recently transitioned our application from Java 11 to Java 17.

Time zone rules change quite often. Most every version and update of Java includes a fresh tzdata.

Subsequent to the deployment, we've noticed that certain records exhibit a discrepancy where dates are displaying with a one-day difference (+1).

Sounds like a time zone issue.

This occurs specifically in records with years before 1940.

By the way… The time zone rules were retroactively documented mainly for 1970 and later. And even then the rules have some holes and inaccuracies. Going further back in decades is even more likely to be less accurate. Fixes are always possible in later versions of tzdata.

Our application operates in the CEST timezone

No such thing as CEST time zone.

Text such as CEST is a pseudo-zone that gives a hint about what might be the intended time zone, and whether Daylight Saving Time (DST) is in effect. Pseudo-zones are not standardized, and are not even unique(!). Never use these pseudo-zones for storing, exchanging, or processing date-time values. Use pseudo-zones only for presentation to the user.

Real time zones have a name in format of Continent/Region such as Europe/Paris, America/Edmonton, and Pacific/Auckland.

dates are stored in MongoDB as UTC.

Exactly what data type did you use? Not covered in your Question.

The provided code snippet, written in Kotlin, produces satisfactory results in Java 11. However, when executed with Java 17, the same code fails to function as expected.

The most likely explanation is that your issue (differing dates) has nothing to do with versions of Java… the issue involves a change in your JVMs’ current default time zone.

For any given moment, the date varies around the globe by time zone. The same moment can be “tomorrow” in Tokyo Japan while simultaneously “yesterday” in Los Angeles US.

Run this to verify the current default time zone (in Java syntax):

System.out.println( ZoneId.systemDefault() ) ;

Depending on such defaults is a great way to mess up your tests. Instead, specify your expected/desired time zone.

System.out.println( Runtime.version() ) ;

Instant instant = Instant.ofEpochMilli( -1_118_625_572_000L ) ;  // Always has an offset of zero hours-minutes-seconds from UTC.

ZoneId zLosAngeles = ZoneId.of( "America/Los_Angeles" ) ;
ZoneId zTokyo = ZoneId.of( "Asia/Tokyo" ) ;

ZonedDateTime zdtLosAngeles = instant.atZone( zLosAngeles ) ;
ZonedDateTime zdtTokyo = instant.atZone( zTokyo ) ;

System.out.println( "Same moment, different dates." );
System.out.println( zdtLosAngeles ) ;
System.out.println( zdtTokyo ) ;

See this code run at Ideone.com. Notice the differing dates in effect for the same simultaneous point on the timeline. Alice in LA looks up to her wall calendar to see the 21st, while at the very same moment Bob in Tokyo looks up to his wall clock/calendar to see the 22nd.

12.0.1+12

Same moment, different dates.

1934-07-21T14:40:28-08:00[America/Los_Angeles]

1934-07-22T07:40:28+09:00[Asia/Tokyo]

Verify inputs

Let's run your examples through Java 12 at Ideone.com.

System.out.println( Runtime.version() ) ;

String v1934_input = "1934-07-21T22:40:28.000+00:00" ;
String v1897_input = "1897-08-06T23:40:28.000+00:00" ; 

Instant v1934_instant = Instant.parse ( v1934_input ) ;
Instant v1897_instant = Instant.parse ( v1897_input ) ;

long v1934_millis = v1934_instant.toEpochMilli() ;
long v1897_millis = v1897_instant.toEpochMilli() ;

System.out.println( v1934_input + " = " + v1934_instant + " = " + v1934_millis ) ;
System.out.println( v1897_input + " = " + v1897_instant + " = " + v1897_millis ) ;

12.0.1+12

1934-07-21T22:40:28.000+00:00 = 1934-07-21T22:40:28Z = -1118625572000

1897-08-06T23:40:28.000+00:00 = 1897-08-06T23:40:28Z = -2284762772000

🌐
Oracle
docs.oracle.com › en › java › javase › 17 › docs › api › java.base › java › time › package-summary.html
java.time (Java SE 17 & JDK 17)
January 20, 2026 - This stores a date-time like '2010-12-03T11:30+01:00'. This is sometimes found in XML messages and other forms of persistence, but contains less information than a full time-zone. Unless otherwise noted, passing a null argument to a constructor or method in any class or interface in this package will cause a NullPointerException to be thrown. The Javadoc "@param" definition is used to summarise the null-behavior.
🌐
Oracle
docs.oracle.com › en › java › javase › 17 › docs › api › java.base › java › util › class-use › Date.html
Uses of Class java.util.Date (Java SE 17 & JDK 17)
January 20, 2026 - Returns the date and time when the timestamp was generated. Constructors in java.security with parameters of type Date
Top answer
1 of 2
6

java.util.Date and java.util.Calendar are not deprecated, and still work as they always have. However, how they work is difficult to use, which is why java.time classes are recommended instead.

What you are seeing is the difference between WEEK_OF_YEAR (which depends on the locale) and ALIGNED_WEEK_OF_YEAR (which is the same in all locales).

When setting or getting the WEEK_OF_MONTH or WEEK_OF_YEAR fields, Calendar must determine the first week of the month or year as a reference point. The first week of a month or year is defined as the earliest seven day period beginning on getFirstDayOfWeek() and containing at least getMinimalDaysInFirstWeek() days of that month or year. Weeks numbered ..., -1, 0 precede the first week; weeks numbered 2, 3,... follow it. Note that the normalized numbering returned by get() may be different. For example, a specific Calendar subclass may designate the week before week 1 of a year as week n of the previous year.


ALIGNED_WEEK_OF_YEAR represents concept of the count of weeks within the period of a year where the weeks are aligned to the start of the year. This field is typically used with ALIGNED_DAY_OF_WEEK_IN_YEAR.

For example, in a calendar systems with a seven day week, the first aligned-week-of-year starts on day-of-year 1, the second aligned-week starts on day-of-year 8, and so on. Thus, day-of-year values 1 to 7 are in aligned-week 1, while day-of-year values 8 to 14 are in aligned-week 2, and so on.


And for the locale difference:

// returns DayOfWeek.MONDAY
WeekFields.of(Locale.forLanguageTag("de-DE")).getFirstDayOfWeek();

// returns DayOfWeek.SUNDAY
WeekFields.of(Locale.forLanguageTag("en-ZA")).getFirstDayOfWeek();

To get the unaligned week of year using the current system local with java.time:

LocalDate.now().get(WeekFields.of(Locale.getDefault()).weekOfYear())

If you want to be locale-independent, then there is ISO.weekOfYear() if you want the week to start on a Monday, and SUNDAY_START.weekOfYear() for a Sunday.

2 of 2
0

The old, much-derided Date and Calendar classes have always been confusing and difficult to use properly, particularly in a multi-threaded context.Java 8’s JSR 310 implementation offers specific classes for:

The old date library included only a single time representation class – java.util.Date, which despite its name, is actually a timestamp. It only stores the number of milliseconds elapsed since the Unix epoch.

Find elsewhere
🌐
Oracle
docs.oracle.com › en › java › javase › 17 › docs › api › java.base › java › text › SimpleDateFormat.html
SimpleDateFormat (Java SE 17 & JDK 17)
January 20, 2026 - Returns the beginning date of the 100-year period 2-digit years are interpreted as being within. Returns: the start of the 100-year period into which two digit years are parsed · Since: 1.2 · See Also: set2DigitYearStart(java.util.Date) public StringBuffer format ·
🌐
Oracle
docs.oracle.com › en › java › javase › 17 › docs › api › java.sql › java › sql › class-use › Date.html
Uses of Class java.sql.Date (Java SE 17 & JDK 17)
January 20, 2026 - Obtains an instance of Date from a LocalDate object with the same year, month and day of month value as the given LocalDate. ... Sets the designated parameter to the given java.sql.Date value using the default time zone of the virtual machine that is running the application.
🌐
Oracle
docs.oracle.com › en › java › javase › 18 › docs › api › java.base › java › util › Date.html
Date (Java SE 18 & JDK 18)
August 18, 2022 - Although the Date class is intended to reflect coordinated universal time (UTC), it may not do so exactly, depending on the host environment of the Java Virtual Machine. Nearly all modern operating systems assume that 1 day = 24 × 60 × 60 = 86400 seconds in all cases.
🌐
End of Life Date
endoflife.date › oracle-jdk
Oracle JDK | endoflife.date
January 21, 2026 - Oracle JDK follows the same cadence as OpenJDK, with a 6-month rapid-release cycle (since the release of Java 10) and a new LTS release every 2 years (since OpenJDK 17, previously every 3 years).
🌐
OpenJDK
openjdk.org › projects › jdk › 17
JDK 17
JDK 17 is the open-source reference implementation of version 17 of the Java SE Platform, as specified by by JSR 390 in the Java Community Process.
🌐
Oracle
docs.oracle.com › en › java › javase › 17 › docs › api › java.base › java › time › class-use › LocalDate.html
Uses of Class java.time.LocalDate (Java SE 17 & JDK 17)
October 20, 2025 - Creates a LocalDate instance using the year, month and day from this Date object. Methods in java.sql with parameters of type LocalDate
🌐
Oracle
java.com › releases
JDK Releases
Prior to April 2022 the rule was Tuesday closest to the 17th of those months. Full details can be found here, along with instructions on how to subscribe to CPU Alert notifications. Feature releases are scheduled for release in the middle of March and September.
🌐
W3Schools
w3schools.com › java › java_date.asp
Java Date and Time
Java does not have a built-in Date class, but we can import the java.time package to work with the date and time API. The package includes many date and time classes.
🌐
Oracle
docs.oracle.com › en › java › javase › 17 › docs › api › java.base › java › time › LocalDateTime.html
LocalDateTime (Java SE 17 & JDK 17)
January 20, 2026 - java.time.LocalDateTime · All ... implements Temporal, TemporalAdjuster, ChronoLocalDateTime<LocalDate>, Serializable · A date-time without a time-zone in the ISO-8601 calendar system, such as 2007-12-03T10:15:30....