
I write technical articles that analyze real world engineering problems through the lens of production systems.
My work focuses on architectural tradeoffs, UI layer system design, performance optimization, and reliability patterns derived from shipping and maintaining scalable applications.
Note: This article was originally published on Medium and is republished here for wider access and long-term reference.
How a seemingly simple feature uncovered the true complexity of global time, and why even industry giants struggle to get it right.
“Users should be able to schedule events across time zones.”
Four months ago, I thought this was straightforward. We’re building a platform where users in New York and Mumbai collaborate daily. They need to schedule sessions, and everyone should see the right time. Standard stuff, right?
Wrong. This feature became a masterclass in distributed systems design, edge case handling, and why time is perhaps the hardest problem in software engineering that nobody talks about.
We were building a filtering system for scheduled events. Simple feature: users filter by date range, status, participant. Should have taken a day.
Instead, we discovered our entire time handling was broken.
The symptoms:
Filter for “January 19” returns events from January 18 and 20
Event shows “9:00 PM” in the UI, but 9 PM where? New York? Mumbai? UTC?
Backend returns 2026-01-19T22:00:00Z, frontend displays it differently for each user
The filter wasn’t working because we had no consistent answer to: “What does this timestamp represent?”
This wasn’t a bug to fix. It was a fundamental architectural gap that would have corrupted data at scale if we shipped without addressing it.
Here’s what made this an urgent architectural decision rather than a “tech debt to fix later”:
Without timezone handling:
Every event scheduled across timezones would be ambiguous
Users editing events would accidentally shift times by hours
Any data already created would need manual correction
DST transitions would silently break recurring events
The business impact of shipping broken:
Users manually converting timezones (failure-prone, frustrating)
Support burden for “wrong time” confusion (unsustainable)
Data integrity issues requiring bulk updates (high-risk migration)
Lost credibility with early users (can’t afford as a new feature)
The tradeoff I faced: Delay shipping by 2 weeks to build timezone handling correctly, or ship fast and inherit months of data cleanup and support tickets.
I chose the delay. Here’s why that was the right architectural call.
Before implementing anything, I studied how companies that have to get this right approach the problem. This wasn’t about copying; it was about understanding the decision space.
Google stores events using dual representation:
{
"start": {
"dateTime": "2026-01-19T22:00:00-05:00",
"timeZone": "America/New_York"
}
}
Why both? Because they solve two different problems. An RFC 3339 timestamp with an offset captures the exact moment in history, while the timezone captures the intent: “this should happen at 10 PM New York time, even if DST rules change.”
This design decision prevents a subtle but critical bug: if DST rules change retroactively (Brazil did this in 2019), events shift with the intent, not stay fixed to historical offsets.
Key insight: Storing offset alone loses scheduling intent. Storing timezone alone loses the absolute moment. You need both.
Stripe deals with global payments across timezones:
{
"created": 1737158400,
"scheduled_for": 1737244800,
"timezone": "America/New_York"
}
All business logic runs on Unix timestamps (UTC). The timezone is metadata for display only.
This works for Stripe because payment deadlines are absolute moments. 11:59 PM UTC is a hard cutoff, regardless of where the user is.
Key insight: When the business logic needs absolute time (sorting, comparisons, deadlines), Unix timestamps eliminate ambiguity.
Outlook does something clever for recurring events:
{
"start": {
"dateTime": "2026-01-19T22:00:00",
"timeZone": "Eastern Standard Time"
}
}
No offset. The dateTime is local to the specified timezone, and conversion happens at read time.
This prevents the “shifting recurring events” problem where a weekly 9 AM meeting suddenly becomes 10 AM after DST transitions.
Key insight: For recurring events, preserving local time intent matters more than fixing to an absolute moment.
Despite different implementations, all three share a fundamental principle:
Store both the moment (when in absolute time) and the context (how humans think about it).
You need both because:
Computers care about moments: Sorting, duration calculations, “is this event in the past?”
Humans care about context: “I meant 9 AM New York time, not 9 AM UTC”
The systems that fail are the ones that try to pick one and derive the other. That’s a lossy conversion.
Given our use case, scheduling sessions where users need to edit events and understand “when” from their perspective, I designed this contract:
{
"scheduledDate": "2026-01-19",
"scheduledTime": "22:00",
"durationMinutes": 60,
"eventTimezone": "America/New_York"
}
The client provides the intended local time and timezone. This is what the user actually selected: “January 19 at 10 PM Eastern.”
{
"eventId": 40,
"startTimeUtc": "2026-01-20T03:00:00Z",
"endTimeUtc": "2026-01-20T04:00:00Z",
"scheduledDateTime": "2026-01-19T22:00:00",
"eventTimezone": "America/New_York",
"durationMinutes": 60
}
The server returns both representations:
startTimeUtc / endTimeUtc: The absolute moment (for sorting, filtering, logic)
scheduledDateTime + eventTimezone: The original intent (for editing, display)
Why this architecture:
Single source of truth for “when”: startTimeUtc is unambiguous for all backend operations
Preserves user intent: scheduledDateTime in eventTimezone shows what the user actually scheduled
No conversion loss: Edit → Save → Display round-trips preserve original values
Filter-friendly: Queries on UTC timestamps are straightforward
Alternative I rejected: Storing only UTC and converting to timezone on read.
Why I rejected it: You lose the original timezone context. If a user schedules “9 AM New York time” and later edits it, what timezone should the form show? Their current browser timezone? The event’s timezone? Without storing the original, you’re guessing.
This causes the exact bug we were trying to prevent: users editing events and accidentally shifting times.
Working with my backend engineer, here’s what we built:
We store four critical pieces:
start_time_utc: The absolute moment (TIMESTAMPTZ in Postgres)
end_time_utc: The absolute end moment
scheduled_datetime: The original local time (TIMESTAMP without timezone)
event_timezone: IANA timezone identifier (e.g., "America/New_York")
The scheduled_datetime has no timezone attached because it's local to event_timezone. Together, they capture intent.
We also store duration_minutes separately because DST transitions can make "1 hour duration" ambiguous when crossing a DST boundary.
I chose Luxon over moment.js or date-fns because:
First-class timezone support via IANA database
Immutable API prevents accidental mutations
Explicit about timezone context (forces you to think correctly)
Better DST transition handling
The critical pattern in our frontend code:
When creating an event:
const handleSubmit = async () => {
const { date, time, timezone } = formData;
// Build local datetime in selected timezone
const localDT = DateTime.fromISO(
`${date}T${time}`,
{ zone: timezone }
);
// Validate it exists (handles DST gaps)
if (!localDT.isValid) {
showError(
`${time} doesn't exist on ${date} in ${timezone} due to DST`
);
return;
}
// Send to server (server converts to UTC)
await api.createEvent({
scheduledDate: date,
scheduledTime: time,
eventTimezone: timezone,
durationMinutes: 60
});
};
Key decision: The client validates that the time exists in the selected timezone before sending to the server. This catches DST edge cases (like 2:30 AM on March 9, 2026 in New York, which doesn’t exist) at the point of user input, not as a cryptic server error.
When displaying events:
const formatEventTime = (
event: Event,
viewerTimezone: string
) => {
// Parse the UTC timestamp
const utcTime = DateTime.fromISO(
event.startTimeUtc,
{ zone: 'utc' }
);
// Convert to viewer's timezone for display
const viewerTime = utcTime.setZone(viewerTimezone);
// Also show original scheduled time for context
const eventTime = DateTime.fromISO(
event.scheduledDateTime,
{ zone: event.eventTimezone }
);
return {
display: viewerTime.toFormat("dd MMM, hh:mm a"),
tooltip: `Scheduled as: ${eventTime.toFormat("hh:mm a")} ${event.eventTimezone}`
};
};
We show times in the viewer’s timezone by default (for convenience), but include a tooltip showing the original scheduled time (for clarity).
When editing events:
const loadEventForEdit = (event: Event) => {
// Use scheduledDateTime in eventTimezone
// NOT the UTC time!
const eventDT = DateTime.fromISO(
event.scheduledDateTime,
{ zone: event.eventTimezone }
);
setFormData({
date: eventDT.toJSDate(),
hour: eventDT.hour,
minute: eventDT.minute,
timezone: event.eventTimezone
});
};
Critical principle: Edit forms always show the event’s timezone, not the viewer’s timezone.
Why this matters: If a Mumbai user edits a “10 PM New York” event, showing it as “8:30 AM next day IST” in the edit form is confusing. They might “fix” it to “8:30 AM today,” which actually shifts the New York time to the previous day. Showing the event’s original timezone prevents this entire class of bugs.
Here’s what separates a working implementation from a robust one:
User input (Mumbai user):
- Date: January 19
- Time: 11:00 PM
- Timezone: America/New_York
UTC conversion: January 20, 4:00 AM UTC
The problem: What date do you show in a table? If you show “January 19” (original date), it contradicts the UTC timestamp. If you show “January 20” (UTC date), users get confused.
Our solution: Show dates in the viewer’s timezone for tables, but preserve the event’s timezone in edit forms. This way:
New York users see: “Jan 19, 11:00 PM”
Mumbai users see: “Jan 20, 9:30 AM” (next day, correctly converted)
Everyone editing sees: “Jan 19, 11:00 PM (America/New_York)” with their local equivalent in a tooltip
On March 9, 2026, clocks in New York jump from 1:59 AM to 3:00 AM. If someone tries to schedule something for 2:30 AM, Luxon returns isValid: false.
Our handling:
Client-side validation catches this immediately
Show user-friendly error: “2:30 AM doesn’t exist on March 9 in America/New_York due to Daylight Saving Time”
Suggest alternative: “Did you mean 3:30 AM?”
We also log these occurrences for analysis. If users hit this frequently, we might add UI guardrails during DST transition weekends.
Most insidious UX problem:
Mumbai user schedules:
- January 19, 11:00 PM in New York
- This is January 20, 9:30 AM in Mumbai
If editing shows "January 20, 9:30 AM":
- User thinks: "Why is this the next day?"
- User "corrects" to "January 19, 9:30 AM"
- Now it's January 18, 11:00 PM in New York ❌
Our solution: Always show times in the event’s timezone when editing, with the viewer’s timezone as reference:
┌─────────────────────────────────────────┐
│ Scheduled: 11:00 PM (America/New_York) │
│ Your timezone: Jan 20, 9:30 AM (IST) │
│ ⚠️ Note: Different day in your zone │
└─────────────────────────────────────────┘
This UI pattern prevents the accidental shift while still giving users context.
Development time: 2 weeks additional implementation + 1 week testing
Code complexity: ~400 additional lines of timezone handling logic
Testing surface: DST transitions, midnight boundaries, timezone picker UX
Created automated test suite with 40+ edge cases
Programmatically validated DST transitions for 2026–2030
Risk of shipping broken data: If we’d shipped without this, early users would have scheduled events in ambiguous timezones
Fixing retroactively would require contacting users to confirm intent
High risk of corrupting data at scale
The decision: Better to delay shipping 2 weeks than inherit months of data cleanup, support tickets, and lost user trust.
Our current approach works for our scale (hundreds of events per day). Here’s what would need to change at 10x or 100x:
Storage overhead:
Dual timestamps add ~16 bytes per event
At 100K events/day: 1.6MB/day additional storage (negligible)
At 10M events/day: 160MB/day (still acceptable on modern databases)
Query performance:
Filtering by start_time_utc is indexed and fast
If we needed to filter by “all events at 9 AM in any timezone,” we’d need additional indexes on scheduled_datetime
Data migration risk:
If we’d shipped without timezone handling, then tried to add it later:
Existing events have no timezone context
Retroactive conversion requires guessing user intent (impossible to do correctly)
Would need to contact every user for confirmation
This is why handling it correctly upfront was critical: the cost of fixing it later grows exponentially with data volume.
Before implementation:
Filters returned incorrect results 40% of the time for cross-timezone events
No way to know what “9 PM” meant in the UI
Edit → Save → Display flow would shift times unpredictably
After implementation:
Zero timezone-related bugs in testing (validated programmatically)
100% accuracy on DST transition edge cases
Edit → Save → Display round-trips preserve original intent
User impact:
Users no longer manually convert timezones (eliminated cognitive load)
Can confidently schedule across New York ↔ Mumbai without confusion
Clear UI showing both event time and “your time” builds trust
Team velocity impact:
Prevented months of support tickets for “wrong time” issues
Avoided high-risk data migration to fix broken timezone data
Created reusable patterns for future timezone-dependent features
If we’d shipped without this:
Estimated 15-20 support tickets/month for timezone confusion
Data corruption requiring manual user outreach for clarification
Loss of credibility with early adopters (can’t afford for a new feature)
The 2-week delay saved us months of support burden and protected data integrity from day one.
This wasn’t just about fixing one feature. It established patterns the team now reuses:
Internal documentation created:
“Timezone Handling Guide” for future features
Test suite template for DST edge cases
API design checklist for time-dependent endpoints
Decisions this informed:
How we handle recurring events (next feature on roadmap)
Webhook timestamp formats (adopted UTC + timezone pattern)
User settings for default timezone preference
Knowledge transfer:
Walked the team through the architecture decision in design review
Shared research on Google/Stripe/Outlook patterns
Now referenced when designing other time-dependent features
The multiplier effect: this decision cost two weeks, but it prevents similar bugs across 5+ future features that touch time. That’s organizational leverage, making the next ten decisions easier, not just solving one problem.
This problem taught me something fundamental about distributed systems:
Most timezone bugs aren’t coding errors. They’re modeling errors, and the code just exposes the flawed model.
Before we fixed the model:
❌ Single timestamp (loses either moment or intent)
❌ Ambiguous “date” field (which timezone?)
❌ Client does all conversion (inconsistent across platforms)
After:
✅ Separate moment (UTC) and context (local + timezone)
✅ Server is source of truth for conversion
✅ Client responsible for display preferences only
This pattern applies beyond timezones: Anywhere you have data that exists in multiple “perspectives” (currencies, units, locales), store both the canonical form and the user’s intent.
With hindsight:
Start with edge cases: I should have built the DST transition test cases before the happy path. We discovered the “non-existent time” edge case late in testing. Building tests first would have caught it in design.
More explicit naming: scheduledDateTime is better than generic naming because it signals "this is local time, not UTC." But we initially used eventDate which was ambiguous. Clear naming prevents misuse.
Timezone validation service: We ended up with timezone validation logic in both frontend and backend. Should have created a shared validation library from the start to ensure consistency.
Document the “why” alongside the “what”: Our API docs explain the fields but don’t explain why we chose this pattern over alternatives. Future engineers won’t understand the tradeoffs we considered.
If you’re implementing timezone-aware scheduling:
[ ] Store both UTC timestamp and original local time + timezone
[ ] Use IANA timezone identifiers (not abbreviations like “EST”)
[ ] Validate times during DST transitions
[ ] Test with timezones that cross midnight boundaries
[ ] Show “different day” warnings in UI when relevant
[ ] Edit forms display in event’s timezone with viewer context
[ ] Document which system (client/server) owns conversion
[ ] Have a rollback plan if timezone logic breaks
The one I’d add after this experience: Build automated tests for DST transitions 3–5 years out. Timezone rules change, and your tests should catch that before users do.
We thought we were building a scheduling feature. We actually built a distributed time coordination system.
The difference matters:
A feature is about implementation (write code that works)
A system is about architecture (design contracts that can’t be misused)
Timezones exposed this distinction because they’re fundamentally about distributed state:
Multiple systems (client, API, database) need shared understanding
Multiple perspectives (event time, viewer time, UTC) exist simultaneously
Multiple failure modes (DST, midnight boundaries, data migration) lurk at scale
Don’t optimize for the code you write today. Optimize for the code someone else maintains in 2 years when timezones rules change, DST policies shift, and your assumptions no longer hold.
That’s what separates good systems from great ones: not clever code, but thoughtful architecture that degrades gracefully and makes the right thing easy.
This article reflects real architectural decisions made during active development. We chose Luxon for timezone handling; production systems should always rely on established libraries rather than custom timezone logic. The principles remain universal.