Every experienced Java developer has a date-related war story. Mine came from a batch financial-reconciliation job that silently skipped three months of records because Calendar.MONTH uses zero-based indexing — January is 0, not 1 — and the off-by-one was invisible in unit tests that only ran against the current month. The bug surfaced in a quarterly audit, not in CI. That afternoon I started migrating our entire codebase to java.time, and I have not looked back since.
The java.time package (JSR-310, shipped in Java 8) is not merely a cleaner calendar API — it is a complete redesign of how Java represents time. Every type is immutable and thread-safe, month numbers start at 1, year offsets are gone, and the API forces you to be explicit about whether a value carries a timezone. This guide covers every migration scenario you will encounter when moving a real codebase from java.util.Date, java.util.Calendar, java.sql.Date, and SimpleDateFormat to their modern equivalents. All code has been tested on Java 21.0.3 (Eclipse Temurin) and is fully backward-compatible to Java 8.
The guide is structured so you can jump to any section independently. If you only need to migrate SQL types for a Hibernate project, jump to Section 6. If you are fixing Jackson serialisation in a REST API, jump to Section 10. The pitfalls section at the end contains the five mistakes I see most often in code reviews — read it before you consider the migration done.
Continue reading java.util.Date and Calendar to java.time: The Complete Migration Guide (Java 8–21)