Notification timing looks trivial at first glance.
You have an event.
You subtract a few minutes.
You schedule a notification.
Simple… right?
In practice, notification timing is one of those problems that quietly accumulates edge cases until your system becomes unpredictable, inconsistent, and hard to reason about.
This post explains why notification timing must be deterministic, what usually goes wrong, and how treating it as a pure computation problem changes everything.
⸻
The illusion of simplicity
Most applications start with logic like this:
• Find the next event
• Schedule a notification X minutes before it
• Label it as today, tomorrow, or later
This works… until it doesn’t.
As soon as your app grows, you run into questions like:
• What happens when the trigger time crosses midnight?
• Should “today” be based on the event time or the notification time?
• What if the user changes timezones?
• What if the reference time is slightly different across platforms?
• Why does Android fire the notification today while iOS labels it tomorrow?
These issues don’t come from bugs.
They come from non-deterministic semantics.
⸻
The core mistake: timeline-based reasoning
A common anti-pattern looks like this:
events.first { isSameDay($0.startTime, tomorrow) }
This feels intuitive — but it’s fundamentally flawed.
Why?
Because it mixes absolute time with human calendar concepts (today / tomorrow) too early.
Once you do that:
• Edge cases multiply
• Different platforms diverge
• “Fixes” become patches instead of rules
⸻
Determinism changes the model
A deterministic system answers one question consistently:
Given the same inputs, will every platform produce the same output?
For notification timing, that means:
• No reliance on UI concepts
• No implicit “today” logic
• No platform-specific date helpers
• No stateful or hidden behavior
Only pure computation.
⸻
A deterministic contract
Here’s the mental model that works:
1. Select the upcoming event
• Based only on absolute time
• event.startTime > referenceTime
• Sorted by startTime
2. Compute trigger time
• triggerTime = event.startTime - leadTime
3. Derive day label
• Compare calendar days of:
• triggerTime
• referenceTime
• In a specific timezone
• Result: today, tomorrow, or later
That’s it.
No guessing.
No heuristics.
No shortcuts.
⸻
Why trigger time matters more than event time
Here’s a subtle but critical insight:
Day labels must be based on the trigger time, not the event time.
Example:
• Event starts at 00:30
• Lead time is 60 minutes
• Trigger fires at 23:30 the previous day
If you label based on the event time, you’ll say tomorrow.
If you label based on the trigger time, you’ll say today — which matches user expectations.
This single rule eliminates a large class of bugs.
⸻
Cross-platform consistency is not optional
If your logic lives:
• partly in Swift
• partly in Kotlin
• partly in JavaScript
…then determinism is mandatory, not a nice-to-have.
Otherwise:
• Notifications fire at different times
• Tests pass on one platform and fail on another
• Bugs become “platform quirks”
A deterministic engine guarantees:
• Same inputs
• Same outputs
• Everywhere
⸻
Treat notification timing as a pure engine
The key architectural decision is this:
Notification timing logic should be headless, stateless, and pure.
It should:
• Not schedule notifications
• Not know about permissions
• Not touch UI
• Not persist anything
Its job is to answer exactly one question:
What is the next upcoming event, and when should I notify the user?
Everything else belongs to the application layer.
⸻
A practical implementation
I ran into these problems repeatedly in production scheduling systems, so I extracted the logic into a small deterministic engine:
Notification Intelligence Engine (NIE)
• Cross-platform (Swift, Kotlin, TypeScript)
• Pure computation
• Timezone-aware
• Backed by shared test vectors
• Identical semantics across platforms
GitHub:
👉 NIE
Even if you don’t use it, the design principles are what matter.
⸻
Final thought
Most notification bugs aren’t about notifications.
They’re about time semantics.
Once you make notification timing deterministic:
• Bugs disappear
• Tests become meaningful
• Cross-platform behavior aligns
• The system becomes easier to reason about
If your app depends on notifications, determinism isn’t overengineering - it’s correctness.
Top comments (0)