Time and Time Zones: UTC Instants, Local Business Dates, DateOnly and TimeProvider
Storing time correctly means separating three concepts: event moments are UTC instants (DateTimeOffset), calendar days are DateOnly, and future local events are wall-clock time plus an IANA zone id. This post shows, with real output, how to turn a tenant's day into a UTC range, a testable clock with TimeProvider, the AT TIME ZONE traps in SQL Server and the date shift in Angular.
Version note: The C# code in this post was tested with .NET 10 and EF Core 10, and the SQL examples with SQL Server 2025.
Guess First: The Leave Requests of November 6
HR Portal's customer in Istanbul receives a "yesterday's leave requests" report every morning. Requests are stored in UTC, and the report selects the rows "dated November 6". There are three requests:
List<LeaveRequestRow> requests =
[
new(1, "Ada", DateTimeOffset.Parse("2026-11-05T21:10:00Z")), // 00:10 on Nov 6 in Istanbul
new(2, "Alan", DateTimeOffset.Parse("2026-11-06T09:45:00Z")), // 12:45 on Nov 6 in Istanbul
new(3, "Grace", DateTimeOffset.Parse("2026-11-06T22:30:00Z")) // 01:30 on Nov 7 in Istanbul
];
DateOnly reportDay = new(2026, 11, 6);
string[] naive = requests
.Where(r => DateOnly.FromDateTime(r.CreatedAt.UtcDateTime) == reportDay)
.Select(r => r.Employee).ToArray();
Console.WriteLine($"Naive (UTC date) {reportDay:yyyy-MM-dd}: {string.Join(", ", naive)}");
public record LeaveRequestRow(int Id, string Employee, DateTimeOffset CreatedAt);
Which names are printed? Which names does the HR specialist in Istanbul mean by "the requests that came in on November 6"? What matters is not how many rows there are but which rows; the answer is a little further down.
Context and Problem: Three Kinds of "Time" Squeezed into One Column
Time is everywhere in HR Portal: the moment a leave request is created, the first day of the leave, an employee's birthday, the time of next month's interview, the cut-off of the end-of-day report. In code most of these are represented by a single type, DateTime, and filled by a single habit: DateTime.Now.
The problem is that these are really three different things. The first is an instant: a single point on the universal timeline. "The request was created at 2026-11-05T21:10:00Z" describes the same moment everywhere in the world. The second is a local business date: a calendar day with no time and no zone. Ada's birthday is May 14; it does not become May 13 when she moves to New York. A leave runs "from March 27 to March 30"; the question is how many days it lasts, not how many hours. The third is a wall-clock time plus a time zone: "April 12, 2027 at 10:00, Istanbul". It points to a future moment, but the UTC equivalent of that moment can only be computed with the rules in force on that day.
Put all three into one type and everything looks right as long as the application runs on one server in one country. The bugs appear when HR Portal is sold to a second country, when the server moves to the cloud, or when the first daylight saving change arrives at a branch in Berlin. And these bugs do not throw exceptions; they just produce wrong data.
Symptoms
You usually hear about this problem from the customer, not from the logs. Typical sentences: "Requests created after midnight show up in the previous day's report." "The employee's birthday email went out a day early." "A leave at the end of March should have been 3 days but only 2 were deducted." "After we moved the server, every time shifted by 3 hours."
In the code you look for these traces: DateTime.Now or DateTime.UtcNow calls in domain code, values read from the database as DateTimeKind.Unspecified and then "fixed" with ToLocalTime(), a time-less value such as a birth date kept as DateTime, SQL defaults written with GETDATE(), the number of days between two dates computed with TotalHours / 24, and date ranges written with BETWEEN as closed intervals. If tests depend on the clock there is one more symptom: tests that turn red at certain hours, the ones people describe as "it only breaks in the nightly CI".
The Naive Solution and Why It Breaks
The answer to the opening report:
Naive (UTC date) 2026-11-06: Alan, Grace
The count is right, the names are wrong. The report says two rows, and the user in Istanbul knows two requests came in on November 6; but they were Ada and Alan. Ada's request was created at 00:10 Istanbul time, on November 6; its UTC date is November 5, so it fell out of the report. Grace's request was created at 01:30 on November 7; in UTC it is still November 6, so it got in. Because the totals match, this bug can go unnoticed for months, until an auditor looks at the individual rows.
The second break point is carrying the comfort of "we store UTC, we're fine" into day arithmetic. An employee in Berlin takes leave from March 27 to March 30. The start and end instants are correctly converted to UTC, and the difference is divided into hours:
TimeZoneInfo berlin = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin");
DateTimeOffset leaveStart = BusinessDay.GetUtcRange(new DateOnly(2026, 3, 27), berlin).StartUtc;
DateTimeOffset leaveEnd = BusinessDay.GetUtcRange(new DateOnly(2026, 3, 30), berlin).StartUtc;
int naiveDays = (int)((leaveEnd - leaveStart).TotalHours / 24);
int realDays = new DateOnly(2026, 3, 30).DayNumber - new DateOnly(2026, 3, 27).DayNumber;
Console.WriteLine($"Leave Mar 27 -> Mar 30, naive: {naiveDays} days, DateOnly: {realDays} days");
2026-03-29 in Berlin lasts 23 hours
Leave Mar 27 -> Mar 30, naive: 2 days, DateOnly: 3 days
March 29, 2026 is the day Berlin switches to summer time; it lasts 23 hours. The three-day leave adds up to 71 hours and (int) rounds it down to 2. One day too few is deducted from the employee's leave balance; if nobody notices, the balance does not add up at year end.
The third is trusting the server's clock. We ran the same two lines on the developer's Mac and in a TZ=UTC environment that mimics a cloud server:
Console.WriteLine($"Local zone: {TimeZoneInfo.Local.Id}");
Console.WriteLine($"DateTime.Now kind: {DateTime.Now.Kind}");
# the developer's Mac
Local zone: Turkey
DateTime.Now kind: Local
# same code with TZ=UTC (like a cloud server)
Local zone: UTC
DateTime.Now kind: Local
DateTime.Now says Local in both environments, but "local" is now somewhere else. The code did not change; only the machine it was deployed to changed, and every "today" calculation shifted by 3 hours.
Options
Everything DateTime.Now and the Server's Clock
The most common starting point. It works while there is one country, one server and one office. The price is shown above: it breaks silently when the server moves, when a second country arrives or when daylight saving changes. It is on the list not as a real option but as a default to escape from.
Storing Instants in UTC (or with Their Offset)
Store "something happened" information, the event time, as an instant: DateTimeOffset in C#, and in SQL Server either datetimeoffset or a datetime2 column whose name ends in Utc. Creation, approval, payment and login moments belong here. Microsoft's guide to choosing date types also recommends DateTimeOffset as the default type for representing a specific point in time.
namespace HrPortal.Data;
public class LeaveRequest
{
public int Id { get; set; }
public DateTimeOffset CreatedAt { get; set; } // instant: datetimeoffset in SQL Server
public DateOnly StartDate { get; set; } // local business date: date
public DateOnly EndDate { get; set; }
}
Storing the Local Business Date with DateOnly
Values that are calendar days (birthday, hire date, leave start and end, invoice date) are not instants. They are stored as DateOnly (.NET 6+) and date in SQL Server; since EF Core 8 the SQL Server provider maps DateOnly directly to date. Day counts are computed from the DayNumber difference; daylight saving never enters that calculation. The limit is this: the notion of "today" is still tied to a zone. DateOnly stores the value, but answering "what day is it in Istanbul today" requires converting an instant into a zone.
Storing the Wall-Clock Time and the Time Zone Id Together
For a future local event (an interview, a meeting, the start of a shift) store the local time the user entered together with the IANA zone id ("Europe/Istanbul"), and compute the UTC equivalent on every read with the current rules. This is the main idea of Jon Skeet's "Storing UTC is not a silver bullet": converting a future time to UTC and storing it means storing the wrong instant if the zone rules change in the meantime.
public sealed record Appointment(string Title, DateTime LocalTime, string TimeZoneId)
{
// the instant is computed with the current zone rules on every read
public DateTimeOffset ToInstant()
{
TimeZoneInfo zone = TimeZoneInfo.FindSystemTimeZoneById(TimeZoneId);
return new DateTimeOffset(TimeZoneInfo.ConvertTimeToUtc(LocalTime, zone), TimeSpan.Zero);
}
}
Implementation: The Three Shapes of Time in HR Portal
Our choice is not one option but all three used together: events are instants, calendar days are DateOnly, future local events are wall-clock time + zone. Every tenant has a TimeZoneId; "today" and "end of day" are always computed in that zone.
Turning the Tenant's Day into a UTC Range
The way to fix the report is not to convert the data into the zone but to convert the tenant's day into a UTC range. That way the indexed column in the database is used as is:
namespace HrPortal.Time;
public static class BusinessDay
{
public static (DateTimeOffset StartUtc, DateTimeOffset EndUtc) GetUtcRange(DateOnly day, TimeZoneInfo zone)
=> (StartOfDay(day, zone), StartOfDay(day.AddDays(1), zone));
private static DateTimeOffset StartOfDay(DateOnly day, TimeZoneInfo zone)
{
DateTime local = day.ToDateTime(TimeOnly.MinValue);
// if midnight falls into a spring-forward gap, take the first valid minute
while (zone.IsInvalidTime(local))
{
local = local.AddMinutes(1);
}
// if midnight happens twice when clocks go back, take the first (larger offset = earlier instant)
TimeSpan offset = zone.IsAmbiguousTime(local)
? zone.GetAmbiguousTimeOffsets(local).Max()
: zone.GetUtcOffset(local);
return new DateTimeOffset(local, offset).ToUniversalTime();
}
}
TimeZoneInfo istanbul = TimeZoneInfo.FindSystemTimeZoneById("Europe/Istanbul");
(DateTimeOffset startUtc, DateTimeOffset endUtc) = BusinessDay.GetUtcRange(reportDay, istanbul);
string[] correct = requests
.Where(r => r.CreatedAt >= startUtc && r.CreatedAt < endUtc)
.Select(r => r.Employee).ToArray();
Console.WriteLine($"Range: [{startUtc:O}, {endUtc:O})");
Console.WriteLine($"Tenant day {reportDay:yyyy-MM-dd}: {string.Join(", ", correct)}");
TimeZoneInfo beirut = TimeZoneInfo.FindSystemTimeZoneById("Asia/Beirut");
Console.WriteLine($"Beirut 2026-03-29 00:00 invalid: {beirut.IsInvalidTime(new DateTime(2026, 3, 29, 0, 0, 0))}");
(DateTimeOffset bs, DateTimeOffset be) = BusinessDay.GetUtcRange(new DateOnly(2026, 3, 29), beirut);
Console.WriteLine($"Beirut 2026-03-29: [{bs:O}, {be:O})");
Range: [2026-11-05T21:00:00.0000000+00:00, 2026-11-06T21:00:00.0000000+00:00)
Tenant day 2026-11-06: Ada, Alan
Beirut 2026-03-29 00:00 invalid: True
Beirut 2026-03-29: [2026-03-28T22:00:00.0000000+00:00, 2026-03-29T21:00:00.0000000+00:00)
The range is half-open: >= startUtc and < endUtc. With BETWEEN or <=, a record created exactly at midnight would appear in two days' reports. The Beirut lines show why the loop is needed: in Beirut summer time starts at midnight, so on March 29, 2026 00:00 never happens and the first moment of the day is 01:00. TimeZoneInfo.ConvertTimeToUtc throws for such a local time; most code meets this case for the first time in production.
Injecting "Now" with TimeProvider
The answer to "what day is today" depends on both the clock and the zone. The TimeProvider abstract class introduced in .NET 8 turns the clock into a dependency; we covered its mechanics in "C# Async and .NET 8: TimeProvider, ConfigureAwaitOptions, Parallel.ForAsync and CancelAsync" in the C# Async series. What matters here is the design: domain code does not use DateTime.Now but the injected TimeProvider and the tenant's zone.
public static class TimeProviderExtensions
{
public static DateOnly TodayIn(this TimeProvider time, TimeZoneInfo zone)
=> DateOnly.FromDateTime(TimeZoneInfo.ConvertTime(time.GetUtcNow(), zone).DateTime);
}
public sealed class LeavePolicy(TimeProvider time)
{
public bool CanStartOn(DateOnly start, TimeZoneInfo tenantZone)
=> start >= time.TodayIn(tenantZone);
}
In tests, FakeTimeProvider from the Microsoft.Extensions.TimeProvider.Testing package puts the clock wherever you want:
FakeTimeProvider clock = new(DateTimeOffset.Parse("2026-11-05T20:30:00Z"));
LeavePolicy policy = new(clock);
DateOnly start = new(2026, 11, 5);
Console.WriteLine($"20:30Z Istanbul today: {clock.TodayIn(istanbul):yyyy-MM-dd}, can start {start:yyyy-MM-dd}: {policy.CanStartOn(start, istanbul)}");
clock.Advance(TimeSpan.FromHours(2));
Console.WriteLine($"22:30Z Istanbul today: {clock.TodayIn(istanbul):yyyy-MM-dd}, can start {start:yyyy-MM-dd}: {policy.CanStartOn(start, istanbul)}");
Console.WriteLine($"22:30Z UTC today: {DateOnly.FromDateTime(clock.GetUtcNow().UtcDateTime):yyyy-MM-dd}");
20:30Z Istanbul today: 2026-11-05, can start 2026-11-05: True
22:30Z Istanbul today: 2026-11-06, can start 2026-11-05: False
22:30Z UTC today: 2026-11-05
In UTC it is 22:30 and the calendar still says November 5; in Istanbul the day has already turned. The "a leave cannot start in the past" rule follows the tenant's day. FakeTimeProvider has an interesting constraint as well: when you try to move the clock back, SetUtcNow throws the exception below; you have to start the scenario at the earlier time and move forward with Advance.
System.ArgumentOutOfRangeException: Cannot go back in time. Current time is 11/05/2026 22:30:00 +00:00. (Parameter 'value')
A Future Appointment and a Rule Change
In 2016 Turkey decided to stay on UTC+3 permanently, and the Europe/Istanbul entry in the tz database was updated accordingly. Let's take a meeting converted to UTC in 2015, under the old rules that expected a return to winter time (UTC+2) in November 2016, and show it with today's rules:
DateTime meetingLocal = new(2016, 11, 15, 10, 0, 0);
TimeSpan offsetAssumedIn2015 = TimeSpan.FromHours(2); // EET winter offset under the old rules
DateTimeOffset storedUtc = new DateTimeOffset(meetingLocal, offsetAssumedIn2015).ToUniversalTime();
DateTimeOffset shownToday = TimeZoneInfo.ConvertTime(storedUtc, istanbul);
Console.WriteLine($"Stored in 2015 as UTC: {storedUtc:O}");
Console.WriteLine($"Shown with today's rules: {shownToday:yyyy-MM-dd HH:mm zzz}");
Console.WriteLine($"Istanbul offset 2015-11-15: {istanbul.GetUtcOffset(new DateTime(2015, 11, 15))}");
Console.WriteLine($"Istanbul offset 2016-11-15: {istanbul.GetUtcOffset(new DateTime(2016, 11, 15))}");
Appointment interview = new("Interview with Ada", new DateTime(2027, 4, 12, 10, 0, 0), "Europe/Istanbul");
Console.WriteLine($"Appointment instant (resolved now): {interview.ToInstant():O}");
Stored in 2015 as UTC: 2016-11-15T08:00:00.0000000+00:00
Shown with today's rules: 2016-11-15 11:00 +03:00
Istanbul offset 2015-11-15: 02:00:00
Istanbul offset 2016-11-15: 03:00:00
Appointment instant (resolved now): 2027-04-12T07:00:00.0000000+00:00
The meeting planned for 10:00 shows up at 11:00. There is no bug in the code; the mistake is "freezing" a future instant into UTC with the rules of the moment it was saved. Appointment, on the other hand, stores the local time and the zone id and computes the instant when it is needed; if the rules change, it finds the right instant automatically.
The SQL Server Side
In SQL Server, GETDATE() returns the server's local time, SYSUTCDATETIME() returns UTC and SYSDATETIMEOFFSET() returns the local time with its offset. In Linux containers the server clock is usually UTC, so all three look the same; on an on-premises Windows server GETDATE() returns Istanbul time and defaults change with the server.
SELECT GETDATE() AS ServerLocal, SYSUTCDATETIME() AS Utc, SYSDATETIMEOFFSET() AS WithOffset;
SELECT Employee,
CreatedAtUtc AT TIME ZONE 'UTC' AT TIME ZONE 'Turkey Standard Time' AS CreatedAtIstanbul
FROM dbo.LeaveRequests;
DECLARE @StartUtc datetime2(0) = '2026-11-05T21:00:00',
@EndUtc datetime2(0) = '2026-11-06T21:00:00';
SELECT Employee FROM dbo.LeaveRequests
WHERE CreatedAtUtc >= @StartUtc AND CreatedAtUtc < @EndUtc;
ServerLocal | Utc | WithOffset
2026-11-07 17:58:38.070 | 2026-11-07 17:58:38.0714822 | 2026-11-07 17:58:38.0714822 +00:00
Employee | CreatedAtIstanbul
Ada | 2026-11-06 00:10:00 +03:00
Alan | 2026-11-06 12:45:00 +03:00
Grace | 2026-11-07 01:30:00 +03:00
Employee
Ada
Alan
AT TIME ZONE is fine for display only; used in a filter it wraps the column in a function and the index cannot be used. The filter still uses the UTC range computed in the application. Two more traps:
SELECT CAST('2026-03-29T02:30:00' AS datetime2(0)) AT TIME ZONE 'W. Europe Standard Time' AS InGap,
CAST('2026-10-25T02:30:00' AS datetime2(0)) AT TIME ZONE 'W. Europe Standard Time' AS Ambiguous;
SELECT * FROM dbo.LeaveRequests WHERE CreatedAtUtc AT TIME ZONE 'Europe/Istanbul' > '2026-01-01';
InGap | Ambiguous
2026-03-29 03:30:00 +02:00 | 2026-10-25 02:30:00 +02:00
Msg 9820, Level 16, State 1, Line 4
The time zone parameter 'Europe/Istanbul' provided to AT TIME ZONE clause is invalid.
SQL Server expects Windows zone names ('W. Europe Standard Time', 'Turkey Standard Time'); an IANA id is an error. On the .NET side TimeZoneInfo.TryConvertIanaIdToWindowsId does the conversion. The second trap: 02:30 inside the spring-forward gap is shifted to 03:30 without an error, and the 02:30 that happens twice is interpreted with the first (summer time) offset. The AT TIME ZONE page on Microsoft Learn describes this behavior explicitly; you should choose it knowingly too.
The API and Angular Side
In the API contract, instants travel as ISO 8601 (RFC 3339) with their offset, and calendar days as time-less yyyy-MM-dd. System.Text.Json writes DateOnly that way:
EmployeeDto dto = new("Ada", new DateOnly(1990, 5, 14), DateTimeOffset.Parse("2026-11-05T22:30:00Z"));
Console.WriteLine(JsonSerializer.Serialize(dto, JsonSerializerOptions.Web));
Console.WriteLine(TimeZoneInfo.TryConvertIanaIdToWindowsId("Europe/Istanbul", out string? windowsId) ? windowsId : "none");
public record EmployeeDto(string Name, DateOnly BirthDate, DateTimeOffset CreatedAt);
{"name":"Ada","birthDate":"1990-05-14","createdAt":"2026-11-05T22:30:00+00:00"}
Turkey Standard Time
The classic trap on the Angular side is the date picker producing a Date at local midnight and that Date being written to JSON as UTC. We ran the same TypeScript code in two different zones:
const picked = new Date(1990, 4, 14); // date picker: local midnight
console.log(JSON.stringify({ birthDate: picked }));
const fromApi = new Date("1990-05-14");
console.log(fromApi.toLocaleDateString("en-CA"));
function toDateOnly(d: Date): string {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
console.log(toDateOnly(picked));
# TZ=Europe/Istanbul
{"birthDate":"1990-05-13T21:00:00.000Z"}
1990-05-14
1990-05-14
# TZ=America/New_York
{"birthDate":"1990-05-14T04:00:00.000Z"}
1990-05-13
1990-05-14
May 14 picked by a user in Istanbul goes to the server as 1990-05-13T21:00:00.000Z; if the server takes its Date part, the birthday becomes May 13. A user in New York who parses the API's "1990-05-14" with new Date() sees May 13, because a time-less ISO date is interpreted as UTC midnight. The fix is the same in both directions: carry the calendar day as a yyyy-MM-dd string without turning it into a Date (in Angular, formatDate(value, 'yyyy-MM-dd', 'en-US') does the same job).
Trade-off Table
| Criterion | Everything DateTime.Now | UTC instant | DateOnly | Wall clock + zone |
|---|---|---|---|---|
| Data integrity | Breaks when the server changes | Complete for events | Complete for calendar days | Complete for future events |
| Query complexity | Looks simple, wrong results | Range math for day boundaries | Simple (date comparison) | Instant computed on every read |
| Performance | Good | Good (indexed range) | Good | "After now" filters are hard in SQL |
| Reversibility | Unknown which zone old data is in | Display zone can change later | Lossless | Lossless, survives rule changes |
| Team cognitive load | Low, until the first bug | Medium | Low | High (zone ids, invalid times) |
When Not to Use It
Do not turn every date value into an instant. Birthday, hire date, invoice date and leave days are DateOnly; storing them as DateTimeOffset pushes the question "midnight in which zone" into the data.
There is no need to store wall-clock time + zone for past events. The UTC equivalent of something that already happened never changes; a rule change only affects future local events. The cognitive load of the wall-clock model pays off only for future events such as appointments, shifts and scheduled campaigns.
For an internal application running in one country whose zone will never change, a per-tenant zone design can be overkill. But even there, using TimeProvider instead of DateTime.Now is cheap insurance: tests become deterministic and the code does not change when the server moves.
Which zone the end-of-day report follows is a business decision, not a technical one. In a multi-country company both the head office's zone and each branch's own zone can be reasonable; do not write the code before the decision is made.
Checklist
- There is no
DateTime.Now,DateTime.UtcNoworDateTimeOffset.Nowin domain code; the clock is injected viaTimeProvider. - Event moments are stored as
DateTimeOffset(or adatetime2whose name ends inUtc), calendar days asDateOnly/date. - Every tenant has an IANA zone id; "today" and day boundaries are computed in that zone.
- Day and period filters are written as half-open ranges (
>= start AND < end); there is noBETWEEN. - Day counts are computed from the
DayNumberdifference; there is noTotalHours / 24. - Future local events are stored as local time + zone id, and the instant is computed at read time.
- In the API, instants are ISO 8601 with an offset and calendar days are
yyyy-MM-dd; in Angular a calendar day is carried without being turned into aDate. - SQL defaults use
SYSUTCDATETIME();AT TIME ZONEis used only for display and with Windows zone names.
Related Patterns and Sources
This post talks directly to two others in the series. The validity ranges in "Temporal Data: Effective-Dated Records, Price History and the Bitemporal Model" rely on the half-open range rule and the zone-aware day boundaries described here. "Sentinel Values: The Hidden Cost of 0, -1, "1900-01-01" and 9999-12-31" explains why fake dates such as DateTime.MinValue and 9999-12-31 should be replaced with null. For TimeProvider itself see "C# Async and .NET 8: TimeProvider, ConfigureAwaitOptions, Parallel.ForAsync and CancelAsync" in the C# Async series, and for EF Core's DateOnly mapping see "What's New in EF Core 8: Complex Types, Primitive Collections and SqlQuery<T>" in the EF Core series. Sources: the Microsoft Learn guide "Choose between DateTime, DateOnly, DateTimeOffset, TimeSpan, TimeOnly, and TimeZoneInfo" and the TimeProvider API documentation; for SQL Server the "AT TIME ZONE (Transact-SQL)" and sys.time_zone_info pages; the IANA Time Zone Database (tz); RFC 3339 for the date format; and Jon Skeet's "Storing UTC is not a silver bullet" on the risk of storing future events in UTC.
A Small Experiment
★☆☆ Run BusinessDay.GetUtcRange for Europe/Berlin on October 25, 2026. How many hours does the day last? With which offsets were the start and end of the range computed?
★★☆ Add the rule "a leave request can be created no later than 17:00 on the day before the leave starts" to LeavePolicy. Write the rule in the tenant's zone and set up two test scenarios with FakeTimeProvider for 16:59 and 17:01.
★★★ Map Appointment with EF Core to LocalTime (datetime2) and TimeZoneId (varchar(64)) columns. Write the query "appointments in the next 24 hours": instead of computing instants in SQL, fetch candidate rows with a coarse range and apply the exact filter in the application. Explain why this two-step query is needed.
We now store time correctly; but a user who clicks "Save" twice, or a client that retries a request after a timeout on a mobile network, can still create the same order twice. In the next post we look at how an idempotency key keeps the same request from running a second time.