I've been digging around in time APIs a bunch lately. One thing I've found over time is that it helps to think of parts of time APIs as contextual in UI. This is similar to Locale or Android's resource system.

You wouldn't usually pass around a localized string in your business logic, that's the UI layer's job! Instead we pass around resource IDs as a source of truth, and the UI layer (Android View, etc) handles mapping that appropriately for rendering to the user. This is important because the UI layer is what actually knows the relevant context the user is reading in (locale, language, time of day, etc).

Time should work the same way, at least with UI presentation. Don't pass around a ZonedDateTime because it mixes contextual information (the zone) with the source of truth (its underlying epoch time). Instead, pass around an Instant and let the UI layer handle mapping that onto whatever its user context is as needed.

There are obviously cases where business logic may want to deal with these other classes directly, but I try to keep them out of any public APIs. In fact, the only time classes that I've ever found myself truly needing in public APIs are Instant and Duration. The rest could be implementation details.

In short - stick with agnostic sources of truth. Don't bring in timezones until the last second.