- TypeScript
- Temporal API
- Booking Systems
Handling UK clock changes in a booking engine with the Temporal API
Twice a year, UK clocks change and careless booking code moves appointments by an hour. How Slotwise keeps instants and wall-clock hours apart with Temporal.
Toukir Ahmed Rony · · 4 min read
Twice a year, UK clocks change, and twice a year badly written booking software quietly moves people's appointments by an hour. Nobody notices until a customer turns up at the wrong time.
When I built Slotwise, an open-source booking engine for UK businesses, I treated clock changes as a first-class problem rather than an edge case. This post explains what goes wrong, the rules I chose, and how the Temporal API made those rules explicit.
What actually happens on the two days
On the last Sunday of March, clocks jump forward from 01:00 GMT to 02:00 BST. Local times from 01:00 to 01:59 simply don't exist that day.
On the last Sunday of October, clocks fall back from 02:00 BST to 01:00 GMT. Local times from 01:00 to 01:59 happen twice.
Code that adds hours to a JavaScript Date, or that stores a business's working hours in UTC, gets one of these days wrong. A clinic that opens at 09:00 suddenly appears to open at 08:00 or 10:00, because "09:00" was saved as a UTC time that only meant 09:00 for half the year.
Two kinds of time, kept apart
The core decision in Slotwise is that there are two different kinds of time, and they are never mixed.
Bookings are instants. An appointment happens at one exact moment, so it is stored as timestamptz in UTC. That never changes meaning.
Working hours are wall-clock times. When a business says it opens at 09:00, it means 09:00 on the clock on the wall in the UK, whatever the season. So working hours are stored as minutes since local midnight in Europe/London. 540 means 09:00 UK time, never UTC.
Every conversion between the two goes through the Temporal API, using temporal-polyfill. Temporal's big advantage is that it forces you to say what kind of value you have: a PlainDate (a calendar day), a wall-clock time, or an Instant (an exact moment). You can't accidentally treat one as another.
Here is the idea in a few lines, simplified from the approach rather than copied from the engine:
import { Temporal } from 'temporal-polyfill';
// Opening time 09:00 on a given UK day, turned into an exact instant.
const day = Temporal.PlainDate.from('2026-10-25');
const opening = day
.toZonedDateTime({ timeZone: 'Europe/London', plainTime: Temporal.PlainTime.from({ hour: 9 }) })
.toInstant();On 25 October 2026 that gives 09:00 UTC, because the clocks have gone back to GMT. On 29 March 2026 the same code gives 08:00 UTC, because the UK is on BST. The business's opening time stays 09:00 in both cases, which is what the customer expects.
Rules written down, not left to defaults
Libraries all have some behaviour for the missing hour and the repeated hour. The problem is that it is usually implicit. I wanted Slotwise's behaviour written down and tested:
- A start time in the spring gap is skipped. No slot is offered from 01:00 to 01:59 on that day.
- A start time in the autumn fold is offered once, at its first (BST) occurrence, so customers never see the same time listed twice.
- Window edges that fall in a gap move to the moment the clocks jump.
- Durations and buffers are real elapsed minutes. A 60-minute appointment always lasts 60 minutes.
The dashboard and booking widget always show business time, labelled "UK time", so nobody has to think about offsets.
I looked at Luxon and date-fns-tz. Both work, but their clock-change behaviour is implicit, and Temporal is native in Node 26. Storing hours as UTC or as TIME columns was never an option: that is the classic bug, where "09:00" changes meaning twice a year.
A pure engine makes it testable
The slot finder lives in packages/engine and is a pure function. It does no database work, no I/O, and doesn't even read the clock: now is passed in as an argument. The API loads working hours, overrides and existing bookings, one query per table, and hands them over.
Because the engine is pure, testing clock changes is easy. There is no need to fake the system time or wait for October. The unit tests run in milliseconds and cover all four clock changes in 2026 and 2027, along with split shifts, buffers, notice periods and overrides. Because 2027 is covered too, nothing can be quietly hard-coded to 2026. As a check on the tests themselves, changing the autumn rule from "first occurrence" to "later occurrence" makes 6 of them fail.
It is fast as well. The engine alone takes about 11 ms for one staff member over 30 days, measured with a Vitest bench, well inside the 200 ms budget I set.
What I took from it
Time bugs come from mixing up "an exact moment" and "a time on the clock". Once those are separate types, and the clock-change rules are explicit and tested, the two awkward Sundays of the year become just two more test cases.
The engine, tests and decision record are on GitHub: github.com/ToukirOfficial/slotwise.
