Part 5 of 8 in the series Time in Software, Done Right
"All stores open at 10:00."
Simple requirement, right? But this sentence hides a massive ambiguity that will determine your entire data model.
Do all stores open at the same moment worldwide (a global event)?
Or does each store open when its local clock shows 10:00 (a local event)?
Same words. Completely different meanings. Completely different storage.
This article will help you spot the difference — and handle the even trickier case of recurring events, where Daylight Saving Time (DST) creates times that don't exist and times that exist twice.
Global Events vs Local Events
Let's make the distinction crystal clear.
Global Event: Same Moment, Different Clocks
A global event happens at one physical instant. Everyone experiences it at the same moment, but their clocks show different times.
Examples:
- Product launch: "New iPhone available at 10:00 AM Pacific" — that's 7:00 PM in Vienna
- Livestream start: "Keynote begins at 18:00 UTC" — same instant everywhere
- Stock market close: The NYSE closes at one moment, regardless of where traders are
- Server maintenance window: "Downtime from 02:00-04:00 UTC"
How to store: Instant (UTC). Everyone converts to their local clock for display.
event_type: global
instant_utc: 2026-06-05T17:00:00Z
When you query "what's happening now?", you compare against the current instant. Done.
Local Event: Same Clock, Different Moments
A local event happens when local clocks show a specific time. The physical moment is different for each timezone.
Examples:
- Store opening hours: "All stores open at 10:00" — means 10:00 in London, 10:00 in Vienna, 10:00 in Tokyo
- TV broadcast: "News at 8 PM" — 8 PM in your timezone
- Daily standup: "Team meeting at 09:00 Vienna time"
- Deadline: "Submit by 23:59 in your local timezone"
How to store: local_start + time_zone_id. Each location has its own timezone.
event_type: local
local_start: 10:00
time_zone_id: (per location)
When the London store opens at 10:00 AM GMT, the Vienna store is still closed (it's 11:00 AM there). The Vienna store opens when its clock shows 10:00.
The Critical Question
When someone says "all X happen at 10:00", ask:
"Do they all happen at the same moment (global), or do they each happen when local clocks show 10:00 (local)?"
The answer changes everything:
| Requirement | Type | Storage |
|---|---|---|
| "Product available at 10:00 Pacific" | Global | Instant |
| "Stores open at 10:00" | Local |
LocalTime + TimeZone per location |
| "Meeting at 14:00 Vienna" | Local | LocalDateTime + TimeZone |
| "Server restart at 03:00 UTC" | Global | Instant |
Get this wrong, and your stores either all open at the wrong time, or your "global launch" happens at different moments in different regions.
Recurring Rules: A Different Beast
Recurring events aren't timestamps at all. They're rules that generate timestamps.
- "Every Monday at 10:00"
- "First Friday of each month"
- "Every day at 03:00"
- "Yearly on December 25th"
You don't store a list of dates. You store the rule, and compute the occurrences when needed.
recurrence_rule: FREQ=WEEKLY;BYDAY=MO
local_time: 10:00
time_zone_id: Europe/Vienna
This rule, combined with the timezone, generates concrete ZonedDateTime instances: Monday June 1st at 10:00 Vienna, Monday June 8th at 10:00 Vienna, and so on.
Why Recurrence Must Be Local
Recurring events are almost always local events.
"Team standup every Monday at 10:00" means 10:00 on the team's wall clock — not a fixed UTC instant that drifts relative to their calendar as DST changes.
If you store a recurring event as "every Monday at 08:00 UTC" (because Vienna is UTC+2 in summer), what happens in winter when Vienna switches to UTC+1? Your 08:00 UTC becomes 09:00 Vienna time. The standup just moved by an hour.
Store the rule in local time. Let the timezone handle DST.
DST Edge Cases: The Hard Part
Here's where recurring rules get interesting. DST creates two nasty edge cases that trip up even experienced teams — if you haven't encountered them yet, you probably will.
Edge Case 1: The Time That Doesn't Exist
In most of Europe, clocks spring forward on the last Sunday of March. At 02:00, clocks jump to 03:00.
The times 02:00, 02:15, 02:30, 02:45 don't exist that day.
What if you have a recurring event at 02:30 every day?
- March 28th at 02:30 Vienna — exists normally
- March 29th at 02:30 Vienna — doesn't exist (clocks skip from 01:59 to 03:00)
- March 30th at 02:30 Vienna — exists normally
What should happen?
Most systems choose one of:
- Skip the occurrence (it's in a gap, treat it as if it didn't happen)
- Shift forward to 03:00 (the first valid moment after the gap)
- Shift backward to 01:59 (the last valid moment before the gap)
NodaTime calls these resolution strategies. There's no universally "correct" answer — it depends on your domain.
A 02:30 AM cron job that does cleanup? Probably shift forward to 03:00.
A 02:30 AM medication reminder? Maybe shift backward — better early than missed.
The point: You need to decide, and your code needs to handle it explicitly.
Edge Case 2: The Time That Exists Twice
In autumn, clocks fall back. In most of Europe, at 03:00 on the last Sunday of October, clocks jump back to 02:00.
The times 02:00 to 02:59 exist twice that day — once before the change, once after.
What if you have a recurring event at 02:30?
- October 25th at 02:30 Vienna — exists once
- October 26th at 02:30 Vienna — exists twice (once in summer time, once in winter time)
- October 27th at 02:30 Vienna — exists once
What should happen?
Options:
- Pick the first occurrence (summer time, before clocks change)
- Pick the second occurrence (winter time, after clocks change)
- Trigger both (if it's a job that should run twice)
Again, domain-dependent. A daily backup at 02:30? Probably run once, pick whichever. A reminder? Probably the first one.
Cron Jobs vs Calendar Events
Cron jobs and calendar events look similar but behave differently with DST.
Cron: Usually System Time (Often UTC)
Traditional cron runs on the system clock. If your server is in UTC, a 0 2 * * * job runs at 02:00 UTC every day — no DST weirdness, but it drifts relative to local calendars.
If your server uses local time, cron will experience DST gaps and overlaps. Most cron implementations:
- Skip jobs that fall into a gap
- Run once for jobs that fall into an overlap (not twice)
But this varies by implementation. Check your system's documentation.
Calendar Events: User Intent
Calendar events are about user intent. "Remind me at 09:00 every day" means 09:00 on my wall clock, regardless of DST.
If you're building a calendar or reminder system, you need:
- Store the rule in local time + timezone
- Compute occurrences by applying the rule to the timezone
- Handle gaps and overlaps with explicit resolution strategies
Holidays: The Complex Recurrence Rules
Some recurring events don't follow simple patterns:
- Easter: First Sunday after the ecclesiastical full moon on or after March 21st (yes, really)
- Thanksgiving (US): Fourth Thursday of November
- Ramadan: Based on the Islamic lunar calendar — starts on a different Gregorian date each year
- Chinese New Year: Based on the lunisolar calendar
For these, you typically need:
- A specialized library (like NodaTime's
IsoDayOfWeekcalculations, or dedicated holiday libraries) - A database of dates (for religious holidays that depend on moon sightings)
- Possibly user input ("Ramadan starts on X this year")
Don't try to compute Easter from scratch. Use a library.
Practical Guidance
For One-Time Events
Ask: "Same moment worldwide, or same local time?"
-
Same moment: Store as
Instant(UTC) -
Same local time: Store as
local_start + time_zone_id
For Recurring Events
- Store the rule, not a list of occurrences
- Store local time + timezone, not UTC
- Decide your DST resolution strategy (skip, shift forward, shift backward)
- Document it — future you will thank present you
For Cron Jobs
- If possible, run in UTC to avoid DST surprises
- If you must use local time, test what happens on DST transition days
- Consider whether a skipped or doubled job matters for your use case
Key Takeaways
- Global events happen at one instant; store as UTC
- Local events happen when local clocks match; store as local time + timezone
- Recurring rules generate timestamps; store the rule, not the instances
- DST creates gaps (times that don't exist) and overlaps (times that exist twice)
- Cron and calendars behave differently with DST — know which you're building
- Complex holidays (Easter, Ramadan) need specialized libraries or data
- When in doubt, ask: "Same moment, or same clock reading?"
Next up: .NET in Practice – Modeling Time with NodaTime — making all this theory real in code.
Top comments (0)