TZ rules are one way, you can derive zoned time reliably from utc, but not the other way around unless you already know the offset. That’s why seemingly storing utc is best, because you can apply whatever tz rule you want. But, because tz rules are decided by politicians (dst, ramadan, date line shifts, it gets pretty weird, …) and often with little advance notice, tz rule databases are often wrong in practice, which means storing only utc and getting zoned time back out of it reliably is hard. That’s why storing iso timestamps with offsets is best. It preserves both utc and intended local (zoned) time at the time of storing, even if the tz database then changes.
But this is not the author’s problem. They’re dealing with wall clock time. They want to store 9 am and have it always be 9 am, no matter what happens with time zones. That is easy: store the timestamp without tz info. But, the hard part is knowing the instant in time (in coordinated time). Wall clock times may occur twice, so there is no single reliable way to get utc out of it, and even when calculating utc the result can be wrong or become wrong due to the political insanity that is zoned time so it is dangerous to store it and rely on it. You see this problem with anything that schedules people or resources. Something scheduled at 9 am is always 9 am, but if the system needs to send a reminder at 9 am, when do you schedule it? There are no solutions I’m aware of without edge cases.
I solved this for a reservation system by storing wall clock time without offsets and the time zone (id) of the resource separately, and then calculating zoned time for that zone and comparing it with the stored wall clock time every time the instant in time mattered. It worked but was tricky to code.