<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: bwi</title>
    <description>The latest articles on Forem by bwi (@bwi).</description>
    <link>https://forem.com/bwi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F280053%2Fe0728482-c6e1-47ca-a2d6-3e2a1e9f0f04.jpeg</url>
      <title>Forem: bwi</title>
      <link>https://forem.com/bwi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/bwi"/>
    <language>en</language>
    <item>
      <title>The Role of a Software Architect – And Why It Often Can't Be Fulfilled</title>
      <dc:creator>bwi</dc:creator>
      <pubDate>Tue, 24 Feb 2026 16:48:16 +0000</pubDate>
      <link>https://forem.com/bwi/the-role-of-a-software-architect-and-why-it-often-cant-be-fulfilled-1ec</link>
      <guid>https://forem.com/bwi/the-role-of-a-software-architect-and-why-it-often-cant-be-fulfilled-1ec</guid>
      <description>&lt;p&gt;In many organizations, the title "Software Architect" exists. It is often used to signal technical maturity — both internally and externally. But maturity doesn't come from naming. It comes from the organizational anchoring of the function.&lt;/p&gt;

&lt;p&gt;Architecture is a design function. It requires &lt;strong&gt;context, influence, time, and a clear mandate&lt;/strong&gt;. If any of these is missing, architecture doesn't emerge — instead, what emerges is the sum of local decisions that no one holds together.&lt;/p&gt;

&lt;p&gt;The question is therefore not: "Do we have an architect?"&lt;br&gt;
But rather: &lt;strong&gt;"Do we have the conditions for architecture to actually happen?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Architecture rarely fails due to a lack of competence.&lt;br&gt;
It fails due to a lack of organizational honesty.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Is Not Implementation with More Experience
&lt;/h2&gt;

&lt;p&gt;A common misconception is the assumption that a software architect is simply a particularly experienced developer — a "Senior Developer Plus."&lt;/p&gt;

&lt;p&gt;Of course, an architect needs technical experience. A great deal of it, in fact.&lt;br&gt;
But their primary task is not writing code.&lt;/p&gt;

&lt;p&gt;Ralph Johnson put it succinctly in a widely cited discussion, later referenced by Martin Fowler in his essay &lt;em&gt;"Who Needs an Architect?"&lt;/em&gt;: "Architecture is the decisions that you wish you could get right early in a project." Why? Because they are hard to change later.&lt;/p&gt;

&lt;p&gt;The architect's task, then, is to make exactly these decisions — decisions that make the code possible and sustainable in the first place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How should the system evolve over the years?&lt;/li&gt;
&lt;li&gt;What structure allows growth without exponential complexity?&lt;/li&gt;
&lt;li&gt;Which decisions are reversible — and which are not?&lt;/li&gt;
&lt;li&gt;Where do future risks arise (scaling, security, operations, compliance)?&lt;/li&gt;
&lt;li&gt;What dependencies are we building right now — and what long-term costs do they create?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not implementation questions.&lt;br&gt;
These are questions of structure, direction, and sustainability.&lt;/p&gt;

&lt;p&gt;Mark Richards and Neal Ford articulated the first law of software architecture: "Everything in software architecture is a trade-off." And the corollary: "If you think you've found something that isn't a trade-off, you probably just haven't identified the trade-off yet."&lt;/p&gt;

&lt;p&gt;A senior developer optimizes the solution.&lt;br&gt;
A software architect shapes the solution space.&lt;/p&gt;

&lt;p&gt;The difference lies not in the ability to implement, but in the focus of responsibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Structural Tension: Responsibility Here, Decisions Everywhere
&lt;/h2&gt;

&lt;p&gt;If architecture concerns this type of decision, the real question is not competence — but whether the organization actually enables these decisions to be made.&lt;/p&gt;

&lt;p&gt;Architecture requires that someone can &lt;strong&gt;see&lt;/strong&gt; and &lt;strong&gt;understand&lt;/strong&gt; the big picture. Not in the sense of control, but in the sense of overview: What capabilities already exist? What initiatives are running in parallel? What integrations, data flows, and operational assumptions are in play?&lt;/p&gt;

&lt;p&gt;In many organizations, responsibilities appear to be neatly divided: Business decides the &lt;em&gt;what&lt;/em&gt;, engineering managers organize the teams, tech leads own day-to-day delivery — and the architect takes care of "the structure."&lt;/p&gt;

&lt;p&gt;Sounds tidy. Often doesn't work.&lt;/p&gt;

&lt;p&gt;Because architecture is not an isolated responsibility. It is the result of decisions being made everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When the business decides to expand into new markets, that has architectural consequences.&lt;/li&gt;
&lt;li&gt;When the team structure changes, it affects whether microservices make sense or a monolith is the better choice.&lt;/li&gt;
&lt;li&gt;When one team introduces a new programming language, another picks a new framework, and a third builds its own deployment pipeline — that creates exactly the island landscape that architecture is supposed to prevent.&lt;/li&gt;
&lt;li&gt;How an application is deployed is an architectural decision. Team boundaries, system boundaries, repository structure, and deployment capabilities are tightly coupled — often as a cycle that extends into SLAs, availability, and compliance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conway articulated this as early as 1968: "Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations." And Ruth Malan stated the consequence: "If the architecture of the system and the architecture of the organization are at odds, the architecture of the organization wins."&lt;/p&gt;

&lt;p&gt;This creates a structural tension: The &lt;strong&gt;responsibility&lt;/strong&gt; for long-term system evolution lies with architecture. The &lt;strong&gt;decision-shaping factors&lt;/strong&gt;, however, are distributed across the organization. The &lt;strong&gt;expectation&lt;/strong&gt; that "the architect will take care of it" exists — even when they weren't involved. And the &lt;strong&gt;mandate&lt;/strong&gt; to participate in the relevant decisions is missing or unclear.&lt;/p&gt;

&lt;p&gt;When these things don't align, a fatal situation arises: Architecture is expected but cannot be actively shaped. It reacts to decisions that have already been made, instead of co-shaping them in advance.&lt;/p&gt;

&lt;p&gt;This is not a competence problem. It is an organizational-structural pattern.&lt;/p&gt;

&lt;p&gt;That is why a software architect must at least be involved in all these areas — not to decide the "whether," but to raise "what does this mean for the system?" in time. &lt;strong&gt;Everything that extends beyond a single team should be architecturally visible.&lt;/strong&gt; Not as an approval process. Not as bureaucracy. But because someone needs to maintain the overview.&lt;/p&gt;

&lt;p&gt;Depending on team size and complexity, architecture can be practiced as a centralized, distributed, or rotating role. The title doesn't matter. What matters is that the &lt;strong&gt;function&lt;/strong&gt; exists and is organizationally anchored.&lt;/p&gt;

&lt;p&gt;When this doesn't happen, you don't see it immediately in the code — but in patterns: Locally sensible solutions emerge that don't fit together globally. Similar things are built multiple times because no one knows they already exist. Every solution brings its own terminology, data models, and interfaces. Integration and operations become more expensive with every island.&lt;/p&gt;

&lt;p&gt;From the outside, this can look as if architecture is "unnecessary." Not because architecture has no value, but because the organization doesn't use it as a connecting function. Then architecture remains a collection of individual decisions that don't arise from a shared vision, but from accidental sequencing and local pressure.&lt;/p&gt;

&lt;p&gt;This is not a criticism of individual developers — it is an observation about organizational mechanics.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Practical Example
&lt;/h2&gt;

&lt;p&gt;A system manages campaigns with start and end dates. In the beginning, a simple &lt;code&gt;DateTime&lt;/code&gt; field suffices. The assumption: All users are in the same time zone, deadlines mean "end of the day," and nobody asks which day exactly is meant.&lt;/p&gt;

&lt;p&gt;This works — as long as the assumption holds.&lt;/p&gt;

&lt;p&gt;Then international customers arrive. A user in New York enters "June 5th" as a deadline. A user in Vienna sees the same date — but whose midnight applies? The system doesn't know, because the &lt;code&gt;DateTime&lt;/code&gt; type doesn't carry this information. It stores a number without context.&lt;/p&gt;

&lt;p&gt;Each team solves the problem locally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The backend team silently converts everything to UTC — but loses the information about which time zone the user intended.&lt;/li&gt;
&lt;li&gt;The frontend team displays dates in the browser's time zone — which may differ from the server's.&lt;/li&gt;
&lt;li&gt;A third team builds a reporting export that outputs the database values directly — in UTC, without conversion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No team made a mistake. Every decision was locally reasonable. But the system now displays three different times for the same event — depending on where you look.&lt;/p&gt;

&lt;p&gt;The real problem lies deeper: "What is a date in our system?" was never asked as a question. Is it a point on the timeline? A calendar day in the user's sense? A deadline that requires a time zone? A recurring event governed by local rules? These are fundamentally different concepts — but the data model treats them all the same: as a single column of type &lt;code&gt;timestamp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Technically, all of this is solvable. But no longer easily. The data model must be split. Existing data must be migrated — without knowing which time zone was intended at the time of entry. Every interface, every report, every display must be reviewed.&lt;/p&gt;

&lt;p&gt;The feature wasn't complex. The missing architectural embedding was. A single early decision — "What types of time exist in our system, and how do we model them?" — would have prevented the entire downstream effort.&lt;/p&gt;

&lt;p&gt;And this is precisely where it becomes clear: Architecture without context can only manage islands — but cannot shape systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Needs Focus and Feedback
&lt;/h2&gt;

&lt;p&gt;Architectural work doesn't happen in the context switch between two bug fixes. It emerges through analysis, modeling, evaluating alternatives, mentally playing through scenarios, and making deliberate decisions under uncertainty.&lt;/p&gt;

&lt;p&gt;Typical components include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documenting decisions (e.g., ADRs)&lt;/li&gt;
&lt;li&gt;Evaluating trade-offs&lt;/li&gt;
&lt;li&gt;Defining guardrails&lt;/li&gt;
&lt;li&gt;Evolution paths and refactoring strategies&lt;/li&gt;
&lt;li&gt;Risk analysis&lt;/li&gt;
&lt;li&gt;Dependency and integration maps&lt;/li&gt;
&lt;li&gt;Communication and alignment across teams&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When 90% of working time is reserved for feature implementation, there is effectively no room for any of this. Then architecture isn't shaped — it emerges as a byproduct. Then a DateTime field becomes a system-wide problem — not because the question was hard, but because nobody had the time to ask it. And that is one of the most expensive states a software system can be in.&lt;/p&gt;

&lt;p&gt;At the same time, architecture also fails at the other extreme: when architects work far removed from day-to-day development. They define guidelines and design target states — without truly knowing the practical constraints of implementation. The result is "ivory tower architecture."&lt;/p&gt;

&lt;p&gt;Good architecture therefore needs both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strategic distance&lt;/strong&gt;, to think long-term.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practical proximity&lt;/strong&gt;, to stay realistic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An architect doesn't code &lt;em&gt;because features need to be delivered&lt;/em&gt;.&lt;br&gt;
They code &lt;em&gt;because architecture must be real&lt;/em&gt; — to validate assumptions, make risks visible, and test structures.&lt;/p&gt;

&lt;p&gt;If either is missing, architecture becomes either reactive — or detached from reality.&lt;/p&gt;

&lt;p&gt;One might argue that reactive architecture can be intentional. Modern architectural approaches deliberately follow the principle of deferring decisions as long as possible — until more information is available.&lt;/p&gt;

&lt;p&gt;The difference lies not in the &lt;em&gt;timing&lt;/em&gt;, but in the &lt;em&gt;reason&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strategy:&lt;/strong&gt; We &lt;em&gt;could&lt;/em&gt; decide earlier but intentionally choose to decide later. Example: We leave the storage technology open but define clear interfaces and performance checks until load profiles are measurable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constraint:&lt;/strong&gt; We &lt;em&gt;have to&lt;/em&gt; react now because we previously lacked information, influence, or time. Example: We pick "some" technology under time pressure, without load assumptions, without guardrails, without an exit path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both look "reactive" from the outside. But they are fundamentally different.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Recognize That Architecture Doesn't Truly Exist
&lt;/h2&gt;

&lt;p&gt;Not as a title. But as a lived function.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are structural decisions consciously documented — or just implemented?&lt;/li&gt;
&lt;li&gt;Is there explicit time for architectural work — or does it happen "when there's a gap"?&lt;/li&gt;
&lt;li&gt;Are architects involved in strategic product decisions?&lt;/li&gt;
&lt;li&gt;Are major changes made visible across teams before implementation?&lt;/li&gt;
&lt;li&gt;Is there transparency about which capabilities and modules already exist?&lt;/li&gt;
&lt;li&gt;Are technical guardrails actively defined — or do they emerge implicitly?&lt;/li&gt;
&lt;li&gt;Are there planned evolution paths — or only refactoring when needed?&lt;/li&gt;
&lt;li&gt;Are long-term risks actively discussed — or only visible when they occur?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If most answers are "no," architecture probably doesn't exist as a shaping function.&lt;/p&gt;

&lt;p&gt;And emergence is no substitute for responsibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A software architect is not simply a senior developer with a different title.&lt;br&gt;
They are responsible for the structure, direction, and future viability of a system.&lt;/p&gt;

&lt;p&gt;For this, they need context, influence, time, and a mandate. If any of these is missing, they cannot fulfill their role — no matter how competent they are.&lt;/p&gt;

&lt;p&gt;That is why every organization that uses the title "Software Architect" should ask itself a simple question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do we truly want to shape architecture?&lt;br&gt;
Or do we just want to pretend it exists?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>career</category>
      <category>leadership</category>
      <category>management</category>
    </item>
    <item>
      <title>Die Aufgabe eines Softwarearchitekten – und warum sie oft nicht stattfinden kann</title>
      <dc:creator>bwi</dc:creator>
      <pubDate>Tue, 24 Feb 2026 16:48:14 +0000</pubDate>
      <link>https://forem.com/bwi/die-aufgabe-eines-softwarearchitekten-und-warum-sie-oft-nicht-stattfinden-kann-42jk</link>
      <guid>https://forem.com/bwi/die-aufgabe-eines-softwarearchitekten-und-warum-sie-oft-nicht-stattfinden-kann-42jk</guid>
      <description>&lt;h1&gt;
  
  
  Die Aufgabe eines Softwarearchitekten – und warum sie oft nicht stattfinden kann
&lt;/h1&gt;

&lt;p&gt;In vielen Unternehmen gibt es den Titel „Softwarearchitekt". Oft wird er verwendet, um technische Reife zu signalisieren — intern wie extern. Doch Reife entsteht nicht durch Benennung. Sie entsteht durch die organisatorische Verankerung der Funktion.&lt;/p&gt;

&lt;p&gt;Architektur ist eine Gestaltungsfunktion. Sie braucht &lt;strong&gt;Kontext, Einfluss, Zeit und ein klares Mandat&lt;/strong&gt;. Fehlt eines davon, entsteht keine Architektur — sondern die Summe lokaler Entscheidungen, die niemand zusammenhält.&lt;/p&gt;

&lt;p&gt;Die Frage ist deshalb nicht: „Haben wir einen Architekten?"&lt;br&gt;
Sondern: &lt;strong&gt;„Haben wir die Voraussetzungen, damit Architektur stattfinden kann?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Architektur scheitert selten an fehlender Kompetenz.&lt;br&gt;
Sie scheitert an fehlender organisationaler Ehrlichkeit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architektur ist keine Implementierung mit mehr Erfahrung
&lt;/h2&gt;

&lt;p&gt;Ein häufiger Irrtum ist die Annahme, ein Softwarearchitekt sei einfach ein besonders erfahrener Entwickler — ein „Senior Developer Plus".&lt;/p&gt;

&lt;p&gt;Natürlich braucht ein Architekt technische Erfahrung. Viel sogar.&lt;br&gt;
Aber seine primäre Aufgabe ist nicht das Schreiben von Code.&lt;/p&gt;

&lt;p&gt;Ralph Johnson brachte es in einer vielzitierten Diskussion auf den Punkt, die Martin Fowler später in seinem Essay &lt;em&gt;„Who Needs an Architect?"&lt;/em&gt; aufgriff: Architektur sind die Entscheidungen, die man gerne früh im Projekt richtig treffen würde — weil sie später schwer zu ändern sind.&lt;/p&gt;

&lt;p&gt;Die Aufgabe eines Architekten ist es also, genau diese Entscheidungen zu treffen — Entscheidungen, die den Code überhaupt erst möglich und nachhaltig machen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wie soll sich das System über Jahre entwickeln?&lt;/li&gt;
&lt;li&gt;Welche Struktur erlaubt Wachstum ohne exponentielle Komplexität?&lt;/li&gt;
&lt;li&gt;Welche Entscheidungen sind reversibel — und welche nicht?&lt;/li&gt;
&lt;li&gt;Wo entstehen zukünftige Risiken (Skalierung, Sicherheit, Betrieb, Compliance)?&lt;/li&gt;
&lt;li&gt;Welche Abhängigkeiten bauen wir gerade auf — und welche langfristigen Kosten entstehen daraus?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Das sind keine Implementierungsfragen.&lt;br&gt;
Das sind Struktur-, Richtungs- und Nachhaltigkeitsfragen.&lt;/p&gt;

&lt;p&gt;Mark Richards und Neal Ford formulierten dazu das erste Gesetz der Softwarearchitektur: Alles in der Softwarearchitektur ist ein Trade-off — und wer glaubt, etwas gefunden zu haben, das keiner ist, hat den Trade-off vermutlich nur noch nicht erkannt.&lt;/p&gt;

&lt;p&gt;Ein Senior Developer optimiert die Lösung.&lt;br&gt;
Ein Softwarearchitekt gestaltet den Lösungsraum.&lt;/p&gt;

&lt;p&gt;Der Unterschied liegt nicht in der Fähigkeit zu implementieren, sondern im Schwerpunkt der Verantwortung.&lt;/p&gt;




&lt;h2&gt;
  
  
  Das strukturelle Spannungsfeld: Verantwortung hier, Entscheidungen überall
&lt;/h2&gt;

&lt;p&gt;Wenn Architektur diese Art von Entscheidungen betrifft, ist die eigentliche Frage nicht Kompetenz — sondern ob die Organisation diese Entscheidungen überhaupt möglich macht.&lt;/p&gt;

&lt;p&gt;Architektur setzt voraus, dass jemand das große Ganze &lt;strong&gt;sehen&lt;/strong&gt; und &lt;strong&gt;wissen&lt;/strong&gt; kann. Nicht im Sinne von Kontrolle, sondern im Sinne von Überblick: Welche Fähigkeiten existieren bereits? Welche Vorhaben laufen parallel? Welche Integrationen, Datenflüsse und Betriebsannahmen sind im Spiel?&lt;/p&gt;

&lt;p&gt;In vielen Organisationen werden Zuständigkeiten scheinbar sauber aufgeteilt: Business entscheidet das &lt;em&gt;Was&lt;/em&gt;, Engineering Manager organisieren die Teams, Tech Leads verantworten die Umsetzung im Alltag — und der Architekt kümmert sich um „die Struktur".&lt;/p&gt;

&lt;p&gt;Klingt ordentlich. Funktioniert so oft nicht.&lt;/p&gt;

&lt;p&gt;Denn Architektur ist keine isolierte Zuständigkeit. Sie ist das Ergebnis von Entscheidungen, die überall getroffen werden:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wenn das Business beschließt, in neue Märkte zu expandieren, hat das architektonische Konsequenzen.&lt;/li&gt;
&lt;li&gt;Wenn die Teamstruktur geändert wird, beeinflusst das, ob Microservices sinnvoll sind oder ein Monolith die bessere Wahl ist.&lt;/li&gt;
&lt;li&gt;Wenn ein Team eine neue Programmiersprache einführt, ein anderes ein neues Framework wählt und ein drittes eine eigene Deployment-Pipeline baut — dann entsteht genau die Insellandschaft, die Architektur verhindern soll.&lt;/li&gt;
&lt;li&gt;Wie eine Applikation deployed wird, ist eine Architekturentscheidung. Teamzuschnitt, Systemschnitt, Repository-Struktur und Deployment-Fähigkeiten hängen eng zusammen — oft als Kreislauf, der bis in SLAs, Verfügbarkeit und Compliance reicht.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conway formulierte das bereits 1968: Organisationen, die Systeme entwerfen, sind gezwungen, Designs zu produzieren, die die Kommunikationsstrukturen dieser Organisationen abbilden. Und Ruth Malan formulierte die Konsequenz: Wenn Systemarchitektur und Organisationsarchitektur im Widerspruch stehen, gewinnt die Organisation.&lt;/p&gt;

&lt;p&gt;So entsteht ein strukturelles Spannungsfeld: Die &lt;strong&gt;Verantwortung&lt;/strong&gt; für die langfristige Systementwicklung liegt bei der Architektur. Die &lt;strong&gt;entscheidungsprägenden Faktoren&lt;/strong&gt; entstehen aber verteilt in der Organisation. Die &lt;strong&gt;Erwartung&lt;/strong&gt;, dass „der Architekt schon dafür sorgt", besteht — auch wenn er gar nicht eingebunden war. Und das &lt;strong&gt;Mandat&lt;/strong&gt;, an den relevanten Stellen mitzuwirken, fehlt oder ist unklar.&lt;/p&gt;

&lt;p&gt;Wenn diese Dinge nicht zusammenpassen, entsteht eine fatale Situation: Architektur wird erwartet, kann aber nicht aktiv gestaltet werden. Sie reagiert auf Entscheidungen, die bereits getroffen wurden, statt sie im Vorfeld mitzuprägen.&lt;/p&gt;

&lt;p&gt;Das ist kein Kompetenzproblem. Es ist ein organisationsstrukturelles Muster.&lt;/p&gt;

&lt;p&gt;Deshalb muss ein Softwarearchitekt in all diesen Bereichen zumindest einbezogen werden — nicht um das „Ob" zu entscheiden, sondern um das „Was bedeutet das für das System?" rechtzeitig einzubringen. &lt;strong&gt;Alles, was über ein einzelnes Team hinausgeht, sollte architektonisch sichtbar sein.&lt;/strong&gt; Nicht als Genehmigungsprozess. Nicht als Bürokratie. Sondern weil jemand den Überblick behalten muss.&lt;/p&gt;

&lt;p&gt;Je nach Teamgröße und Komplexität kann Architektur als Rolle zentral, verteilt oder rotierend gelebt werden. Wichtig ist nicht der Titel. Wichtig ist, dass die &lt;strong&gt;Funktion&lt;/strong&gt; existiert und organisatorisch verankert ist.&lt;/p&gt;

&lt;p&gt;Wenn das nicht passiert, sieht man es nicht sofort im Code — sondern in Mustern: Es entstehen lokal sinnvolle Lösungen, die global nicht zusammenpassen. Ähnliche Dinge werden mehrfach gebaut, weil niemand weiß, dass es sie schon gibt. Jede Lösung bringt ihre eigenen Begriffe, Datenmodelle und Schnittstellen mit. Integration und Betrieb werden mit jeder Insel teurer.&lt;/p&gt;

&lt;p&gt;Von außen kann das so wirken, als sei Architektur „unnötig". Nicht, weil Architektur keinen Wert hätte, sondern weil die Organisation sie nicht als verbindende Funktion nutzt. Dann bleibt Architektur eine Sammlung von Einzelentscheidungen, die nicht aus einem gemeinsamen Bild heraus entstehen, sondern aus zufälliger Reihenfolge und lokalem Druck.&lt;/p&gt;

&lt;p&gt;Das ist keine Kritik an einzelnen Entwickler:innen — es ist eine Beobachtung über Organisationsmechanik.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ein Beispiel aus der Praxis
&lt;/h2&gt;

&lt;p&gt;Ein System verwaltet Kampagnen mit Start- und Enddaten. Am Anfang reicht ein einfaches &lt;code&gt;DateTime&lt;/code&gt;-Feld. Die Annahme: Alle Nutzer sitzen in derselben Zeitzone, Deadlines sind „Ende des Tages", und niemand fragt, welcher Tag genau gemeint ist.&lt;/p&gt;

&lt;p&gt;Das funktioniert — solange die Annahme stimmt.&lt;/p&gt;

&lt;p&gt;Dann kommen internationale Kunden. Ein Nutzer in New York gibt „5. Juni" als Deadline ein. Ein Nutzer in Wien sieht dasselbe Datum — aber wessen Mitternacht gilt? Das System weiß es nicht, weil der Typ &lt;code&gt;DateTime&lt;/code&gt; diese Information nicht trägt. Er speichert eine Zahl ohne Kontext.&lt;/p&gt;

&lt;p&gt;Jedes Team löst das Problem lokal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Das Backend-Team konvertiert alles stillschweigend nach UTC — verliert dabei aber die Information, welche Zeitzone der Nutzer gemeint hat.&lt;/li&gt;
&lt;li&gt;Das Frontend-Team zeigt Daten in der Browser-Zeitzone an — die sich vom Server unterscheiden kann.&lt;/li&gt;
&lt;li&gt;Ein drittes Team baut einen Reporting-Export, der die Datenbank-Werte direkt ausgibt — in UTC, ohne Umrechnung.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kein Team hat einen Fehler gemacht. Jede Entscheidung war lokal nachvollziehbar. Aber das System zeigt jetzt für dasselbe Ereignis drei verschiedene Uhrzeiten an — je nachdem, wo man hinschaut.&lt;/p&gt;

&lt;p&gt;Das eigentliche Problem liegt tiefer: „Was ist ein Datum in unserem System?" wurde nie als Frage gestellt. Ist es ein Zeitpunkt auf dem Zeitstrahl? Ein Kalendertag im Sinne des Nutzers? Eine Deadline, die eine Zeitzone braucht? Ein wiederkehrendes Ereignis, das sich nach lokalen Regeln richtet? Das sind fundamental verschiedene Konzepte — aber das Datenmodell behandelt sie alle gleich: als eine einzelne Spalte vom Typ &lt;code&gt;timestamp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Technisch ist all das lösbar. Aber nicht mehr einfach. Das Datenmodell muss aufgespalten werden. Bestehende Daten müssen migriert werden — ohne zu wissen, welche Zeitzone bei der Eingabe gemeint war. Jede Schnittstelle, jeder Report, jede Anzeige muss geprüft werden.&lt;/p&gt;

&lt;p&gt;Nicht das Feature war komplex. Die fehlende architektonische Einbettung war es. Eine einzige frühe Entscheidung — „Welche Typen von Zeit gibt es in unserem System, und wie modellieren wir sie?" — hätte den gesamten Folgeaufwand verhindert.&lt;/p&gt;

&lt;p&gt;Und genau hier zeigt sich: Architektur ohne Kontext kann nur Inseln verwalten — aber keine Systeme gestalten.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architektur braucht Fokus und Feedback
&lt;/h2&gt;

&lt;p&gt;Architekturarbeit entsteht nicht im Task-Wechsel zwischen zwei Bugfixes, sondern durch Analyse, Modellierung, Bewertung von Alternativen, gedankliches Durchspielen von Szenarien und bewusste Entscheidungen unter Unsicherheit.&lt;/p&gt;

&lt;p&gt;Typische Bestandteile sind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dokumentation von Entscheidungen (z. B. ADRs)&lt;/li&gt;
&lt;li&gt;Bewertung von Trade-offs&lt;/li&gt;
&lt;li&gt;Definition von Leitplanken&lt;/li&gt;
&lt;li&gt;Evolutionspfade und Refactoring-Strategien&lt;/li&gt;
&lt;li&gt;Risikoanalyse&lt;/li&gt;
&lt;li&gt;Abhängigkeits- und Integrationslandkarten&lt;/li&gt;
&lt;li&gt;Kommunikation und Alignment über Teams hinweg&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wenn 90 % der Arbeitszeit für Feature-Implementierung reserviert sind, bleibt für all das faktisch kein Raum. Dann wird Architektur nicht gestaltet — sie entsteht als Nebenprodukt. Dann wird aus einem DateTime-Feld ein systemweites Problem — nicht weil die Frage schwer war, sondern weil niemand die Zeit hatte, sie zu stellen. Und das ist einer der teuersten Zustände, die ein Softwaresystem haben kann.&lt;/p&gt;

&lt;p&gt;Gleichzeitig scheitert Architektur auch am anderen Ende: wenn Architekten weit entfernt von der täglichen Entwicklung arbeiten. Sie definieren Leitlinien und entwerfen Zielbilder — ohne die praktischen Einschränkungen der Umsetzung wirklich zu kennen. Die Folge ist „Elfenbeinturm-Architektur".&lt;/p&gt;

&lt;p&gt;Gute Architektur braucht deshalb beides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strategischen Abstand&lt;/strong&gt;, um langfristig zu denken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Praktische Nähe&lt;/strong&gt;, um realistisch zu bleiben.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ein Architekt programmiert nicht, &lt;em&gt;weil Features umgesetzt werden müssen&lt;/em&gt;.&lt;br&gt;
Er programmiert, &lt;em&gt;weil Architektur real sein muss&lt;/em&gt; — um Annahmen zu überprüfen, Risiken sichtbar zu machen und Strukturen zu erproben.&lt;/p&gt;

&lt;p&gt;Fehlt eines davon, wird Architektur entweder reaktiv — oder realitätsfern.&lt;/p&gt;

&lt;p&gt;Man könnte einwenden, dass reaktive Architektur gewollt sein kann. Moderne Architekturansätze verfolgen bewusst das Prinzip, Entscheidungen möglichst spät zu treffen — dann, wenn mehr Informationen verfügbar sind.&lt;/p&gt;

&lt;p&gt;Der Unterschied liegt nicht im &lt;em&gt;Zeitpunkt&lt;/em&gt;, sondern im &lt;em&gt;Grund&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strategie:&lt;/strong&gt; Wir &lt;em&gt;könnten&lt;/em&gt; früher entscheiden, tun es aber absichtlich später. Beispiel: Wir lassen die Storage-Technologie offen, definieren aber klare Schnittstellen und Performance-Checks, bis Lastprofile messbar sind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zwang:&lt;/strong&gt; Wir &lt;em&gt;müssen&lt;/em&gt; jetzt reagieren, weil uns vorher Informationen, Einfluss oder Zeit gefehlt haben. Beispiel: Wir wählen „irgendeine" Technologie unter Zeitdruck, ohne Lastannahmen, ohne Leitplanken, ohne Ausstiegspfad.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Beides sieht von außen „reaktiv" aus. Ist aber fundamental verschieden.&lt;/p&gt;




&lt;h2&gt;
  
  
  Woran du erkennst, dass Architektur nicht wirklich existiert
&lt;/h2&gt;

&lt;p&gt;Nicht als Titel. Sondern als gelebte Funktion.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Werden strukturelle Entscheidungen bewusst dokumentiert — oder nur umgesetzt?&lt;/li&gt;
&lt;li&gt;Gibt es explizite Zeit für Architekturarbeit — oder passiert sie „wenn gerade Luft ist"?&lt;/li&gt;
&lt;li&gt;Sind Architekten in strategische Produktentscheidungen eingebunden?&lt;/li&gt;
&lt;li&gt;Werden größere Änderungen vor Umsetzung teamübergreifend sichtbar gemacht?&lt;/li&gt;
&lt;li&gt;Gibt es Transparenz darüber, welche Fähigkeiten und Module bereits existieren?&lt;/li&gt;
&lt;li&gt;Werden technische Leitplanken aktiv definiert — oder entstehen sie implizit?&lt;/li&gt;
&lt;li&gt;Gibt es geplante Evolutionspfade — oder nur Refactorings bei Bedarf?&lt;/li&gt;
&lt;li&gt;Werden langfristige Risiken aktiv diskutiert — oder erst sichtbar, wenn sie auftreten?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wenn die meisten Antworten „nein" sind, existiert Architektur vermutlich nicht als gestaltende Funktion.&lt;/p&gt;

&lt;p&gt;Und Emergenz ist kein Ersatz für Verantwortung.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fazit
&lt;/h2&gt;

&lt;p&gt;Ein Softwarearchitekt ist nicht einfach ein Senior Developer mit anderem Titel.&lt;br&gt;
Er ist verantwortlich für Struktur, Richtung und Zukunftsfähigkeit eines Systems.&lt;/p&gt;

&lt;p&gt;Dafür braucht er Kontext, Einfluss, Zeit und ein Mandat. Wenn davon etwas fehlt, kann er seine Aufgabe nicht erfüllen — egal wie kompetent er ist.&lt;/p&gt;

&lt;p&gt;Deshalb sollte jedes Unternehmen, das den Titel „Softwarearchitekt" verwendet, sich eine einfache Frage stellen:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wollen wir Architektur wirklich gestalten?&lt;br&gt;
Oder wollen wir nur so tun, als gäbe es sie?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>career</category>
      <category>management</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Showing a Date Needs Three Decisions</title>
      <dc:creator>bwi</dc:creator>
      <pubDate>Mon, 09 Feb 2026 17:36:07 +0000</pubDate>
      <link>https://forem.com/bwi/showing-a-date-needs-three-decisions-1j8d</link>
      <guid>https://forem.com/bwi/showing-a-date-needs-three-decisions-1j8d</guid>
      <description>&lt;h2&gt;
  
  
  Why showing a date always involves three implicit decisions
&lt;/h2&gt;

&lt;p&gt;Date representations feel trivial.&lt;br&gt;
They are everywhere.&lt;br&gt;
And because of that, they are rarely questioned.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MM/dd/yyyy
dd.MM.yyyy
dd. MMMM yyyy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They look harmless.&lt;br&gt;
They look technical.&lt;br&gt;
They look solved.&lt;/p&gt;

&lt;p&gt;But they aren't.&lt;/p&gt;

&lt;p&gt;Behind every date representation are &lt;strong&gt;decisions&lt;/strong&gt; — and most systems make them implicitly.&lt;/p&gt;


&lt;h2&gt;
  
  
  The core insight
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A date representation always consists of three parameters. Always.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Format&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Language&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Region / cultural conventions&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Even if we do not specify them explicitly.&lt;br&gt;
Even if we rely on defaults.&lt;/p&gt;

&lt;p&gt;Defaults do not remove these parameters.&lt;br&gt;
They merely hide them.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Format — structure
&lt;/h2&gt;

&lt;p&gt;The format defines &lt;strong&gt;how the date is structured&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dd.MM.yyyy
MM/dd/yyyy
dd. MMMM yyyy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;order (day / month / year)&lt;/li&gt;
&lt;li&gt;separators (&lt;code&gt;.&lt;/code&gt;, &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;,&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;numeric vs textual representation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it does &lt;strong&gt;not&lt;/strong&gt; define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;language&lt;/li&gt;
&lt;li&gt;region&lt;/li&gt;
&lt;li&gt;cultural expectation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A format describes &lt;strong&gt;shape&lt;/strong&gt;, not meaning.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Language — words
&lt;/h2&gt;

&lt;p&gt;Language determines &lt;strong&gt;which words are used&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;January / Januar / Jänner&lt;/li&gt;
&lt;li&gt;Monday / Montag&lt;/li&gt;
&lt;li&gt;March / März&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But language alone says nothing about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;date order&lt;/li&gt;
&lt;li&gt;separators&lt;/li&gt;
&lt;li&gt;customary notation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"German" is not enough.&lt;br&gt;
"English" is not enough.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Region / cultural conventions — expectations
&lt;/h2&gt;

&lt;p&gt;The region defines &lt;strong&gt;what people expect to see&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dd.MM.yyyy&lt;/code&gt; vs &lt;code&gt;MM/dd/yyyy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;dots vs slashes&lt;/li&gt;
&lt;li&gt;typical short and long forms&lt;/li&gt;
&lt;li&gt;week start&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two users can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;speak the same language&lt;/li&gt;
&lt;li&gt;read the same month name&lt;/li&gt;
&lt;li&gt;and still expect &lt;strong&gt;different representations&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  These three parameters always exist
&lt;/h2&gt;

&lt;p&gt;Even when we only specify one.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;en
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What does this actually mean?&lt;/p&gt;

&lt;p&gt;In most systems, it implicitly becomes something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Language: English
Region: US
Format: MM/dd/yyyy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But these are &lt;strong&gt;assumptions&lt;/strong&gt;, not facts.&lt;/p&gt;

&lt;p&gt;No one explicitly chose them.&lt;br&gt;
No one documented them.&lt;br&gt;
No one can reliably explain them later.&lt;/p&gt;


&lt;h2&gt;
  
  
  Language + region as locale tags (IETF BCP 47)
&lt;/h2&gt;

&lt;p&gt;So we have three parameters — but in practice, two of them (&lt;strong&gt;language&lt;/strong&gt; and &lt;strong&gt;region&lt;/strong&gt;) are usually expressed together as a &lt;strong&gt;locale tag&lt;/strong&gt;.&lt;br&gt;
A locale tag does not replace the format — it only covers language and region.&lt;br&gt;
Most platforms follow a standard for this: &lt;strong&gt;IETF BCP 47&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So when we write &lt;code&gt;de-AT&lt;/code&gt; or &lt;code&gt;en-US&lt;/code&gt;, we are not just picking a language.&lt;br&gt;
We are selecting a language &lt;strong&gt;and&lt;/strong&gt; a regional set of conventions.&lt;/p&gt;

&lt;p&gt;A (common) BCP 47 tag has the form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;language-region
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Language&lt;/strong&gt;: ISO 639 (usually lowercase), e.g. &lt;code&gt;de&lt;/code&gt;, &lt;code&gt;en&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Region&lt;/strong&gt;: ISO 3166 (usually uppercase), e.g. &lt;code&gt;AT&lt;/code&gt;, &lt;code&gt;US&lt;/code&gt;, &lt;code&gt;GB&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part: &lt;strong&gt;language and region are independent dimensions&lt;/strong&gt;.&lt;br&gt;
Any combination can be meaningful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;en-AT&lt;/code&gt; — English language, Austrian conventions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;de-US&lt;/code&gt; — German language, US conventions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also note the order: &lt;code&gt;AT&lt;/code&gt; is a &lt;strong&gt;region&lt;/strong&gt;, so it comes second (&lt;code&gt;en-AT&lt;/code&gt;), not first.&lt;/p&gt;

&lt;p&gt;Some systems use an underscore notation like &lt;code&gt;en_AT&lt;/code&gt;.&lt;br&gt;
In the browser &lt;code&gt;Intl&lt;/code&gt; APIs, the standard form is the hyphen: &lt;code&gt;en-AT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But even a full locale tag only covers two of the three decisions. The format is still a separate choice.&lt;/p&gt;


&lt;h2&gt;
  
  
  Defaults are not decisions
&lt;/h2&gt;

&lt;p&gt;When we specify only part of the information, the system fills in the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;browser&lt;/li&gt;
&lt;li&gt;operating system&lt;/li&gt;
&lt;li&gt;runtime&lt;/li&gt;
&lt;li&gt;library&lt;/li&gt;
&lt;li&gt;framework&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each layer makes a reasonable guess.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Fallbacks do not replace missing parameters.&lt;br&gt;
They only hide them.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Internally, there is still:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a concrete format&lt;/li&gt;
&lt;li&gt;a concrete language&lt;/li&gt;
&lt;li&gt;a concrete region&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are just implicit.&lt;/p&gt;


&lt;h2&gt;
  
  
  Example 1: &lt;code&gt;dd. MMMM yyyy&lt;/code&gt;
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Format: dd. MMMM yyyy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This only means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Write the month name in full."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But which one?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;January&lt;/li&gt;
&lt;li&gt;Januar&lt;/li&gt;
&lt;li&gt;Jänner&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without language &lt;strong&gt;and&lt;/strong&gt; region, the format alone is meaningless.&lt;/p&gt;


&lt;h2&gt;
  
  
  Example 2: German is not just German
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Language: German
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;What does that imply?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;de-DE&lt;/code&gt; → &lt;em&gt;Januar&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;de-AT&lt;/code&gt; → &lt;em&gt;Jänner&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are correct.&lt;br&gt;
Both are German.&lt;br&gt;
And yet, they are &lt;strong&gt;not interchangeable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Language alone is insufficient.&lt;/p&gt;


&lt;h2&gt;
  
  
  Example 3: English is not a culture
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Locale: en-AT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Language: English&lt;/li&gt;
&lt;li&gt;Region: Austria&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A perfectly valid and realistic scenario:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;English UI&lt;/li&gt;
&lt;li&gt;Austrian date conventions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;English does not imply US culture.&lt;br&gt;
It never did.&lt;/p&gt;


&lt;h2&gt;
  
  
  These parameters can come from different sources
&lt;/h2&gt;

&lt;p&gt;In real systems, they rarely originate from one place.&lt;/p&gt;

&lt;p&gt;Typical sources include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser / OS&lt;/strong&gt; — implicit, uncontrolled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User profile&lt;/strong&gt; — explicit, but often incomplete&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin / organization&lt;/strong&gt; — standardized, intentional&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context / use case&lt;/strong&gt; — UI, export, contract, audit log&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider a real scenario:&lt;/p&gt;

&lt;p&gt;A user's browser is set to &lt;code&gt;de-DE&lt;/code&gt;.&lt;br&gt;
Their organization is based in Austria.&lt;br&gt;
An admin has configured dates to display in &lt;code&gt;long&lt;/code&gt; format.&lt;/p&gt;

&lt;p&gt;If the system resolves the locale from the browser, the user sees Januar. If it resolves from the organization, they see Jänner. Which one wins depends on the implementation — and that's rarely documented.&lt;/p&gt;

&lt;p&gt;Either way, someone sees a date that doesn't match their expectation.&lt;/p&gt;

&lt;p&gt;There is no single "correct" date.&lt;/p&gt;


&lt;h2&gt;
  
  
  Even the format is a decision
&lt;/h2&gt;

&lt;p&gt;Most platforms offer predefined format levels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;short
medium
long
full
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But you can also define a fully custom format like &lt;code&gt;dd. MMMM yyyy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Either way, the choice of format is not a technical detail.&lt;br&gt;
It is a &lt;strong&gt;product decision&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;In which context do users see a short date? When do they see a long one?&lt;br&gt;
Who decides — the developer, the admin, the product?&lt;/p&gt;

&lt;p&gt;These are not implementation details. They belong to product design, configuration, and context — not just code.&lt;/p&gt;


&lt;h2&gt;
  
  
  The common mistake
&lt;/h2&gt;

&lt;p&gt;We say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We support localization."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And what we actually mean is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a format&lt;/li&gt;
&lt;li&gt;a default locale&lt;/li&gt;
&lt;li&gt;some Intl APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What we really have is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;a collection of implicit assumptions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And those assumptions have consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A contract rendered with &lt;code&gt;01/02/2025&lt;/code&gt; — is that January 2nd or February 1st?
&lt;/li&gt;
&lt;li&gt;A user in Vienna sees &lt;em&gt;Januar&lt;/em&gt; and wonders why the system doesn't know their country.
&lt;/li&gt;
&lt;li&gt;An audit log stores dates in the server's locale — which changes after a migration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not edge cases.&lt;br&gt;
They are the natural result of treating three decisions as one.&lt;/p&gt;


&lt;h2&gt;
  
  
  The key takeaway
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A date is only reasonably represented when&lt;br&gt;
format, language, and region are all explicitly defined.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything else is guessing.&lt;/p&gt;



&lt;p&gt;Once you accept that these three decisions always exist, the remaining question is no longer if you should define them — but where.&lt;/p&gt;
&lt;h2&gt;
  
  
  What to do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Always pass &lt;strong&gt;format, language, and region&lt;/strong&gt; explicitly — never rely on one to imply the others.&lt;/li&gt;
&lt;li&gt;Treat format selection (&lt;code&gt;short&lt;/code&gt;, &lt;code&gt;long&lt;/code&gt;, &lt;code&gt;full&lt;/code&gt;) as a &lt;strong&gt;product decision&lt;/strong&gt;, not a developer default.&lt;/li&gt;
&lt;li&gt;Define a clear &lt;strong&gt;priority&lt;/strong&gt; for where each parameter comes from: user profile, tenant, admin policy, or context.&lt;/li&gt;
&lt;li&gt;Never persist or export dates in a locale-dependent format without documenting &lt;strong&gt;which&lt;/strong&gt; locale.&lt;/li&gt;
&lt;li&gt;When in doubt, make the decision &lt;strong&gt;visible&lt;/strong&gt; — in configuration, in documentation, in code.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  In the browser: &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The browser is one of the most common user interfaces — and it already has an API that models all three decisions explicitly: &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Format: &lt;code&gt;dateStyle&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;dateStyle&lt;/code&gt; option controls the &lt;strong&gt;shape&lt;/strong&gt; of the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2025-01-02&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;de-AT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dateStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;short&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → "02.01.25"&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;de-AT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dateStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → "02.01.2025"&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;de-AT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dateStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;long&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → "2. Jänner 2025"&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;de-AT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dateStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;full&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → "Donnerstag, 2. Jänner 2025"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same locale. Only the format changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Language + Region: the locale tag
&lt;/h3&gt;

&lt;p&gt;The locale tag is where &lt;strong&gt;language&lt;/strong&gt; and &lt;strong&gt;region&lt;/strong&gt; are combined (BCP 47).&lt;br&gt;
In &lt;code&gt;Intl&lt;/code&gt;, use the hyphen form: &lt;code&gt;de-AT&lt;/code&gt; (not &lt;code&gt;de_AT&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2025-01-02&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;de-DE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dateStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;long&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → "2. Januar 2025"&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;de-AT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dateStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;long&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → "2. Jänner 2025"&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dateStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;long&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → "January 2, 2025"&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-GB&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dateStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;long&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → "2 January 2025"&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-AT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dateStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;long&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → "2 January 2025"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same date. Same format level. Completely different output — because language and region differ.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens when we leave things out?
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2025-01-02&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;// → depends on the browser's region assumption&lt;/span&gt;
&lt;span class="c1"&gt;// Could be "1/2/2025" (US) or "02/01/2025" (GB)&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2025-01-02&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;// → depends entirely on the browser/OS default locale&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is exactly the problem: the API works — but &lt;strong&gt;only if we pass all three decisions explicitly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When we leave the locale vague or omit &lt;code&gt;dateStyle&lt;/code&gt;, the browser fills in the gaps. Silently. Differently on every machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  The API already supports this
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; does not force us to guess. It lets us express all three decisions in a single call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;de-AT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dateStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;long&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Format&lt;/strong&gt;: &lt;code&gt;long&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language&lt;/strong&gt;: German&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Region&lt;/strong&gt;: Austria&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three parameters. One line. No ambiguity.&lt;/p&gt;

&lt;p&gt;The tools exist. The question is whether we use them — or let the defaults decide for us.&lt;/p&gt;

&lt;p&gt;Server-side runtimes follow the same model (for example: &lt;code&gt;CultureInfo&lt;/code&gt; in .NET, &lt;code&gt;Locale&lt;/code&gt; in Java) — but that's a topic for another post.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Every date representation requires &lt;strong&gt;three parameters&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;They exist even when we do not specify them&lt;/li&gt;
&lt;li&gt;Defaults are assumptions, not decisions&lt;/li&gt;
&lt;li&gt;Implicit decisions are the most dangerous ones&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Localization does not start with APIs.&lt;br&gt;
It starts where a product takes responsibility.&lt;/p&gt;

</description>
      <category>softwaredevelopment</category>
      <category>ui</category>
      <category>ux</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Configuration Needs Semantics</title>
      <dc:creator>bwi</dc:creator>
      <pubDate>Fri, 30 Jan 2026 00:06:04 +0000</pubDate>
      <link>https://forem.com/bwi/configuration-needs-semantics-4ii3</link>
      <guid>https://forem.com/bwi/configuration-needs-semantics-4ii3</guid>
      <description>&lt;p&gt;Configuration is everywhere.&lt;/p&gt;

&lt;p&gt;It's the mechanism that lets applications change behavior without recompiling, redeploying, or rewriting code.&lt;br&gt;
Timeouts, limits, feature switches, credentials, rollout toggles — configuration is how software adapts to its environment.&lt;/p&gt;

&lt;p&gt;And anything that's everywhere is easy to treat as trivial.&lt;/p&gt;

&lt;p&gt;In many systems, configuration consists of a boolean here, a string there, maybe a rule evaluated at runtime.&lt;br&gt;
And this works surprisingly well. You can build entire products on top of nothing more than key–value pairs.&lt;/p&gt;

&lt;p&gt;But while the code may not care, the people working with the system do.&lt;br&gt;
And that is where problems begin.&lt;/p&gt;

&lt;p&gt;This article is not about how to implement configuration. It is about &lt;strong&gt;why configuration needs semantics&lt;/strong&gt; — and what happens when it doesn’t.&lt;/p&gt;


&lt;h2&gt;
  
  
  Everything Could Be Configuration
&lt;/h2&gt;

&lt;p&gt;Imagine a small billing service with a single &lt;code&gt;config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"paymentTimeoutSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enableNewCheckoutFlow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"goldPlanAccess"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stripeApiKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sk_live_..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Technically, these are just values.&lt;/p&gt;

&lt;p&gt;Semantically, these values represent very different kinds of concerns:&lt;br&gt;
some describe behavior,&lt;br&gt;
some control rollout,&lt;br&gt;
some grant access,&lt;br&gt;
and some should be treated as confidential.&lt;/p&gt;

&lt;p&gt;From a purely technical perspective, all of them could be modeled the same way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;true&lt;/code&gt; / &lt;code&gt;false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;strings&lt;/li&gt;
&lt;li&gt;numbers&lt;/li&gt;
&lt;li&gt;rules evaluated against context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And in many systems, they &lt;em&gt;are&lt;/em&gt; exactly that.&lt;/p&gt;

&lt;p&gt;This is appealing because it is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;simple&lt;/li&gt;
&lt;li&gt;flexible&lt;/li&gt;
&lt;li&gt;uniform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But this uniformity comes at a cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Works Technically — and Fails Mentally
&lt;/h2&gt;

&lt;p&gt;The runtime does not care &lt;em&gt;why&lt;/em&gt; a value exists.&lt;br&gt;
It only cares &lt;em&gt;what&lt;/em&gt; the value is.&lt;/p&gt;

&lt;p&gt;But humans are different.&lt;/p&gt;

&lt;p&gt;When developers, architects, or product owners make decisions based on configuration, they inevitably form expectations.&lt;/p&gt;

&lt;p&gt;In many systems, configuration is just a JSON file, an environment variable,&lt;br&gt;
or a key–value store.&lt;br&gt;
From that alone, you cannot tell whether a value represents one kind of configuration or another.&lt;/p&gt;

&lt;p&gt;You also cannot answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how long something is expected to exist&lt;/li&gt;
&lt;li&gt;who owns it&lt;/li&gt;
&lt;li&gt;whether it may be removed&lt;/li&gt;
&lt;li&gt;whether it is temporary or permanent&lt;/li&gt;
&lt;li&gt;whether it is safe to change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The configuration &lt;em&gt;source&lt;/em&gt; is semantically silent.&lt;br&gt;
Expectations come not from the file, but from &lt;strong&gt;how values are used in code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If everything is consumed the same way, then everything &lt;em&gt;means&lt;/em&gt; the same thing.&lt;/p&gt;

&lt;p&gt;This is not a technical failure.&lt;br&gt;
It is a &lt;strong&gt;mental model failure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Meaning does not come from data types or storage.&lt;br&gt;
It comes from intent.&lt;/p&gt;




&lt;h2&gt;
  
  
  Semantics Through Explicit Categories
&lt;/h2&gt;

&lt;p&gt;To avoid this, configuration needs more than values.&lt;br&gt;
It needs &lt;strong&gt;meaning&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One way to provide that meaning is by using explicit categories.&lt;br&gt;
Not because the system &lt;em&gt;requires&lt;/em&gt; them — but because &lt;strong&gt;humans do&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Below are four common categories that often get conflated, despite answering very different questions.&lt;/p&gt;




&lt;h3&gt;
  
  
  Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Defines:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Baseline system behavior.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;usually stable&lt;/li&gt;
&lt;li&gt;changed infrequently&lt;/li&gt;
&lt;li&gt;no inherent expiration&lt;/li&gt;
&lt;li&gt;not security-sensitive&lt;/li&gt;
&lt;li&gt;not contractually relevant&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;timeouts&lt;/li&gt;
&lt;li&gt;thresholds&lt;/li&gt;
&lt;li&gt;limits&lt;/li&gt;
&lt;li&gt;algorithm parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Configuration sets the &lt;em&gt;baseline behavior&lt;/em&gt; of a system.&lt;/p&gt;




&lt;h3&gt;
  
  
  Secrets
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Defines:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Confidential data that must be protected.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;security boundaries&lt;/li&gt;
&lt;li&gt;different storage requirements&lt;/li&gt;
&lt;li&gt;restricted access&lt;/li&gt;
&lt;li&gt;often rotated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;passwords&lt;/li&gt;
&lt;li&gt;API keys&lt;/li&gt;
&lt;li&gt;certificates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A secret may be “just a string” technically — but semantically it represents a &lt;strong&gt;security constraint&lt;/strong&gt;, not behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A secret stored as a string will eventually be logged — not out of malice, but out of invisibility.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Feature Flags
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Defines:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Temporary activation of behavior.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explicitly temporary&lt;/li&gt;
&lt;li&gt;expected to disappear&lt;/li&gt;
&lt;li&gt;often tied to rollout or experimentation&lt;/li&gt;
&lt;li&gt;create maintenance pressure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A crucial aspect of feature flags is &lt;strong&gt;expected end-of-life&lt;/strong&gt;.  &lt;/p&gt;

&lt;p&gt;Just calling something a feature flag does not make it temporary.&lt;br&gt;
Only active, operational pressure to remove it does.&lt;/p&gt;

&lt;p&gt;Without expiration tracking, warnings, or any mechanism that makes overstaying visible, you have not created a feature flag — you have created configuration with a different label.&lt;/p&gt;

&lt;p&gt;In other words, the distinction between a feature flag and permanent configuration is not semantic, but operational: if nothing tracks expiry, notifies you of neglect, or surfaces overdue removal, then "feature flag" is just a name that was given once, not a property the system still enforces.&lt;/p&gt;

&lt;p&gt;And this expectation strongly influences how developers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;review code&lt;/li&gt;
&lt;li&gt;accept shortcuts&lt;/li&gt;
&lt;li&gt;tolerate complexity&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Entitlements
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Defines:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Long-term access to behavior.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stable&lt;/li&gt;
&lt;li&gt;contractually relevant&lt;/li&gt;
&lt;li&gt;audit-sensitive&lt;/li&gt;
&lt;li&gt;rarely removed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Entitlements represent &lt;strong&gt;permissions&lt;/strong&gt;, not rollout state.&lt;br&gt;
They are expected to persist.&lt;/p&gt;




&lt;h2&gt;
  
  
  Same Mechanism, Different Meaning
&lt;/h2&gt;

&lt;p&gt;From an implementation perspective, feature flags and entitlements may look identical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a boolean&lt;/li&gt;
&lt;li&gt;a rule evaluated against context&lt;/li&gt;
&lt;li&gt;a configuration value&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But semantically, they create &lt;strong&gt;very different expectations&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Feature Flag&lt;/th&gt;
&lt;th&gt;Entitlement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Expected lifetime&lt;/td&gt;
&lt;td&gt;Temporary&lt;/td&gt;
&lt;td&gt;Long-term&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Removal&lt;/td&gt;
&lt;td&gt;Expected&lt;/td&gt;
&lt;td&gt;Rare&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review strictness&lt;/td&gt;
&lt;td&gt;Often relaxed&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit relevance*&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;*Audit relevance here refers to contractual, legal, or billing-related accountability —&lt;br&gt;
not technical logging.&lt;/p&gt;

&lt;p&gt;These categories are not immutable — but &lt;strong&gt;moving between them should be an explicit decision, not an accident&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When these distinctions are blurred, systems don’t break immediately.&lt;br&gt;
They slowly accumulate misunderstandings.&lt;/p&gt;

&lt;p&gt;In many SaaS systems, a single capability goes through both stages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;During rollout, it is guarded by a &lt;strong&gt;feature flag&lt;/strong&gt; (for example, &lt;code&gt;enableNewCheckoutFlow&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Once stable and part of a paid plan, access is governed by an &lt;strong&gt;entitlement&lt;/strong&gt; (for example, &lt;code&gt;goldPlanAccess&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are different concerns and often belong to different systems of record.&lt;br&gt;
Confusing them leads to permanent flags and unclear contracts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Maintenance, Planning, and Self-Deception
&lt;/h2&gt;

&lt;p&gt;The most dangerous failure mode is not technical debt.&lt;br&gt;
It is &lt;strong&gt;self-deception&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When something temporary is modeled as permanent — or vice versa — teams begin to lie to themselves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“We’ll clean this up later.”&lt;/li&gt;
&lt;li&gt;“This is just a flag.”&lt;/li&gt;
&lt;li&gt;“It’s basically a permission.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code still compiles.&lt;br&gt;
The system still runs.&lt;/p&gt;

&lt;p&gt;But planning becomes unreliable, and maintenance decisions quietly lose clarity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Warnings as Support, Not Enforcement
&lt;/h2&gt;

&lt;p&gt;Explicit semantics are not about forcing behavior.&lt;br&gt;
They are about making assumptions visible.&lt;/p&gt;

&lt;p&gt;Instead of blocking changes, systems can surface signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;warnings when temporary decisions overstay their intent&lt;/li&gt;
&lt;li&gt;growing visibility when cleanup is deferred&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing is blocked.&lt;br&gt;
Nothing breaks.&lt;/p&gt;

&lt;p&gt;But the system no longer allows decisions to remain silent.&lt;/p&gt;

&lt;p&gt;This protects developers and architects by turning vague pressure into explicit signals.&lt;/p&gt;

&lt;p&gt;This is not enforcement.&lt;br&gt;
It is a way to make assumptions and technical debt visible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Configuration is not just about values.&lt;br&gt;
It is about &lt;strong&gt;expectations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Without semantics, configuration becomes a generic container where everything looks the same — and therefore means nothing.&lt;/p&gt;

&lt;p&gt;Explicit categories do not make systems rigid.&lt;br&gt;
They make intentions visible.&lt;/p&gt;

&lt;p&gt;And visibility is often the difference between maintainable systems and ones that quietly drift out of control.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>devops</category>
      <category>discuss</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Indeterminate Is Not a Value</title>
      <dc:creator>bwi</dc:creator>
      <pubDate>Wed, 28 Jan 2026 19:13:05 +0000</pubDate>
      <link>https://forem.com/bwi/indeterminate-is-not-a-value-1omd</link>
      <guid>https://forem.com/bwi/indeterminate-is-not-a-value-1omd</guid>
      <description>&lt;h1&gt;
  
  
  Indeterminate Is Not a Value
&lt;/h1&gt;

&lt;p&gt;In &lt;a href="https://dev.to/bwi/why-checkboxes-are-not-as-simple-as-they-seem-3jhf"&gt;Part 1&lt;/a&gt;, we established what a checkbox actually is: a binary control with presence-based semantics. We learned that "unchecked" doesn't mean "No" — it means "no participation."&lt;/p&gt;

&lt;p&gt;Sometimes you need to represent more than two business states. Some UIs use a checkbox with a dash (—) or filled square for that.&lt;/p&gt;

&lt;p&gt;This article explains what &lt;code&gt;indeterminate&lt;/code&gt; means in HTML, what it does not mean, and the two distinct scenarios where it is commonly used.&lt;/p&gt;

&lt;p&gt;Later, it also introduces a cleaner alternative when you truly need three business states: the participation pattern (checkbox + separate value control).&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: Indeterminate Is Display-Only
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Indeterminate Looks Like
&lt;/h3&gt;

&lt;p&gt;You've probably seen it: a checkbox that's neither checked nor unchecked, but shows a dash (—) or a filled square. This is the &lt;strong&gt;indeterminate&lt;/strong&gt; state.&lt;/p&gt;

&lt;p&gt;It looks like a third state. It &lt;em&gt;feels&lt;/em&gt; like a third state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is not a third state.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What the Specification Actually Says
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://html.spec.whatwg.org/multipage/input.html#checkbox-state-(type=checkbox)" rel="noopener noreferrer"&gt;HTML specification&lt;/a&gt; is explicit:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;indeterminate&lt;/code&gt; IDL attribute [...] gives the appearance of a third state.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Key word: &lt;strong&gt;appearance&lt;/strong&gt;. It &lt;em&gt;looks&lt;/em&gt; like a third state, but technically it isn't one.&lt;/p&gt;

&lt;p&gt;And from &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/indeterminate" rel="noopener noreferrer"&gt;MDN&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;indeterminate&lt;/code&gt; is a &lt;strong&gt;JavaScript property only&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;There is &lt;strong&gt;no HTML attribute&lt;/strong&gt; for it&lt;/li&gt;
&lt;li&gt;It has &lt;strong&gt;no effect on form submission&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;It only changes &lt;strong&gt;how the checkbox looks&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Indeterminate is not a value. It's a visualization.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  You Cannot Set It in HTML
&lt;/h3&gt;

&lt;p&gt;This doesn't work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- This does nothing --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;indeterminate&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can only set it via JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;checkbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indeterminate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What Happens When the User Clicks?
&lt;/h3&gt;

&lt;p&gt;Behavior on click: When the user clicks an indeterminate checkbox, the browser &lt;strong&gt;immediately clears&lt;/strong&gt; the indeterminate state and toggles &lt;code&gt;checked&lt;/code&gt; as normal.&lt;/p&gt;

&lt;p&gt;The user cannot "select" indeterminate. They can only move away from it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: Two Completely Different Reasons for Indeterminate
&lt;/h2&gt;

&lt;p&gt;There are &lt;strong&gt;two completely different reasons&lt;/strong&gt; to show an indeterminate checkbox — and they should &lt;strong&gt;not&lt;/strong&gt; be treated as the same thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reason A: Mixed (Aggregation)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Scenario:&lt;/strong&gt; A parent checkbox with multiple child checkboxes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All children checked → Parent shows checked&lt;/li&gt;
&lt;li&gt;All children unchecked → Parent shows unchecked&lt;/li&gt;
&lt;li&gt;Some children checked, some not → Parent shows &lt;strong&gt;indeterminate&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What indeterminate means here:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The children have different values."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Key characteristics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The parent has &lt;strong&gt;no value of its own&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The visual state is &lt;strong&gt;computed&lt;/strong&gt; from children&lt;/li&gt;
&lt;li&gt;Clicking the parent is a &lt;strong&gt;command&lt;/strong&gt; (set all children to true/false)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing should be stored&lt;/strong&gt; for the parent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the &lt;em&gt;correct&lt;/em&gt; use of indeterminate as defined by &lt;a href="https://www.w3.org/TR/wai-aria-1.1/#aria-checked" rel="noopener noreferrer"&gt;ARIA&lt;/a&gt; (the accessibility standard for web applications):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;aria-checked="mixed"&lt;/code&gt; indicates a mixed mode value for a tri-state checkbox.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Reason B: Unset (Unknown/Never Answered)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Scenario:&lt;/strong&gt; A checkbox that has never been interacted with.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User has never clicked it&lt;/li&gt;
&lt;li&gt;No default value was set&lt;/li&gt;
&lt;li&gt;The system doesn't know the answer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What indeterminate means here:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"No decision has been made yet."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Important distinction:&lt;/strong&gt; From here on, we are no longer talking about the checkbox itself. The checkbox is still binary — it's still either ticked or not ticked. We are talking about the &lt;strong&gt;business meaning&lt;/strong&gt; that the checkbox represents — what it means for your application or process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key characteristics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There is &lt;strong&gt;no business value yet&lt;/strong&gt; — not "yes", not "no", but &lt;em&gt;"we don't know"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;It's about &lt;strong&gt;user interaction history&lt;/strong&gt;, not aggregation&lt;/li&gt;
&lt;li&gt;First click should set a value (typically &lt;code&gt;true&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The absence itself might be significant (e.g., "never answered" vs "answered no")&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Problem: Same Visual, Different Meaning
&lt;/h3&gt;

&lt;p&gt;Both scenarios often use the same visual representation — the indeterminate dash.&lt;/p&gt;

&lt;p&gt;But they mean completely different things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Mixed (Aggregation)&lt;/th&gt;
&lt;th&gt;Unset (Unknown)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Computed from children&lt;/td&gt;
&lt;td&gt;Missing data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Has a domain value?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No (derived)&lt;/td&gt;
&lt;td&gt;No (not yet decided)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Should be stored?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;td&gt;Maybe (as "unknown")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User can select it?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What click does&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Command to children&lt;/td&gt;
&lt;td&gt;Sets initial value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When a system uses the same checkbox component for both without making the distinction explicit, bugs follow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: The Value Question
&lt;/h2&gt;

&lt;p&gt;A common question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If a checkbox is indeterminate, what is its value?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Answer
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A checkbox doesn't have an "indeterminate" value.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It has either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;true&lt;/code&gt; (checked)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;false&lt;/code&gt; (unchecked)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;no value&lt;/strong&gt; (the concept is modeled elsewhere)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;indeterminate&lt;/code&gt; is purely &lt;strong&gt;how it's displayed&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  For Mixed (Parent Checkbox)
&lt;/h3&gt;

&lt;p&gt;The parent checkbox has &lt;strong&gt;no value of its own&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Parent state is DERIVED&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getParentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;indeterminate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;indeterminate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;indeterminate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// checked still exists but is visually suppressed when indeterminate is true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: When &lt;code&gt;indeterminate&lt;/code&gt; is true, the &lt;code&gt;checked&lt;/code&gt; value is &lt;strong&gt;visually hidden&lt;/strong&gt; but still exists. It just doesn't represent anything meaningful.&lt;/p&gt;

&lt;h3&gt;
  
  
  For Unset (Unknown)
&lt;/h3&gt;

&lt;p&gt;The value is &lt;strong&gt;outside the checkbox&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Domain model&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// UI mapping&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toCheckboxState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;indeterminate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;indeterminate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;undefined&lt;/code&gt; lives in your model, not in the checkbox.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The checkbox never becomes tri-state. Only the meaning we project onto it does.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 4: The Participation Pattern
&lt;/h2&gt;

&lt;p&gt;Remember from Part 1: "Unchecked means no participation."&lt;/p&gt;

&lt;p&gt;What if you actually need three states?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Yes&lt;/li&gt;
&lt;li&gt;No&lt;/li&gt;
&lt;li&gt;I don't want to answer / Not applicable&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A single checkbox &lt;strong&gt;cannot do this&lt;/strong&gt;. But two controls can:&lt;/p&gt;

&lt;h3&gt;
  
  
  The Pattern: Checkbox + Value Control
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"participation"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    I want to answer this question
&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;fieldset&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"answer"&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"radio"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"vote"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"yes"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; Yes&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"radio"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"vote"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"no"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; No&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/fieldset&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;participation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;participation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Semantics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checkbox unchecked → "I don't want to participate" (no value sent)&lt;/li&gt;
&lt;li&gt;Checkbox checked + Yes → "Yes"&lt;/li&gt;
&lt;li&gt;Checkbox checked + No → "No"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This cleanly separates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Participation&lt;/strong&gt; (checkbox)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Value&lt;/strong&gt; (radio/select)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why This Is Better Than Tri-State
&lt;/h3&gt;

&lt;p&gt;A tri-state checkbox tries to pack three meanings into one control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checked = Yes&lt;/li&gt;
&lt;li&gt;Unchecked = No&lt;/li&gt;
&lt;li&gt;Indeterminate = Unknown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But as we've seen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Indeterminate has &lt;strong&gt;no value&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The visual is &lt;strong&gt;ambiguous&lt;/strong&gt; (mixed vs unknown)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The participation pattern is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explicit&lt;/li&gt;
&lt;li&gt;Accessible&lt;/li&gt;
&lt;li&gt;Unambiguous&lt;/li&gt;
&lt;li&gt;Actually works with HTML semantics&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 5: When to Use What
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Use a Simple Checkbox When:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You need a binary opt-in (newsletter subscription)&lt;/li&gt;
&lt;li&gt;"Not selected" means "does not apply" (consent checkboxes)&lt;/li&gt;
&lt;li&gt;You're toggling a UI feature (show/hide advanced options)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use Parent/Child Checkboxes When:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You have a hierarchical selection (folder contents)&lt;/li&gt;
&lt;li&gt;"Select all" functionality&lt;/li&gt;
&lt;li&gt;Parent state is &lt;strong&gt;always computed&lt;/strong&gt;, never stored&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use the Participation Pattern When:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;"No answer" is meaningfully different from "No"&lt;/li&gt;
&lt;li&gt;You need to track whether something was explicitly answered&lt;/li&gt;
&lt;li&gt;The question is optional but the answer must be definitive&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use Radio Buttons When:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You need explicit Yes/No&lt;/li&gt;
&lt;li&gt;All options are mutually exclusive&lt;/li&gt;
&lt;li&gt;"No selection" should not be possible&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use a Select/Dropdown When:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You have more than 2-3 options&lt;/li&gt;
&lt;li&gt;You need a "Please select..." placeholder&lt;/li&gt;
&lt;li&gt;Space is constrained&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 6: Practical Rules
&lt;/h2&gt;

&lt;p&gt;Based on everything we've discussed, here are practical rules for keeping checkbox semantics consistent:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Indeterminate Is Display-Only
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Never try to store "indeterminate" as a value.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's a visualization. Treat it that way.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Mixed Should Be Computed
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Parent checkbox state = function of children. Always.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're storing a parent checkbox value alongside its children, you're creating a consistency problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Separate "Unknown" from "Mixed"
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;If both concepts exist in your system, they need different UI treatments.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Same visual + different meaning = bugs.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. When In Doubt, Add a Control
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;If a checkbox feels overloaded, it probably is.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The participation pattern (checkbox + value control) is almost always clearer than a tri-state checkbox.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Make Assumptions Explicit
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Document what "unchecked" means in your system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Is it &lt;code&gt;false&lt;/code&gt;? Is it &lt;code&gt;null&lt;/code&gt;? Is it "not answered"? Is it "does not participate"?&lt;/p&gt;

&lt;p&gt;If you can't answer this question clearly, neither can the next developer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;What It Is&lt;/th&gt;
&lt;th&gt;What It's For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;checked&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Boolean property&lt;/td&gt;
&lt;td&gt;The actual state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;indeterminate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Visual hint (JS only)&lt;/td&gt;
&lt;td&gt;Showing "mixed" or "unknown"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixed state&lt;/td&gt;
&lt;td&gt;Computed from children&lt;/td&gt;
&lt;td&gt;Parent/child hierarchies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown/Unset&lt;/td&gt;
&lt;td&gt;Missing value in model&lt;/td&gt;
&lt;td&gt;Optional questions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Participation pattern&lt;/td&gt;
&lt;td&gt;Checkbox + separate control&lt;/td&gt;
&lt;td&gt;When you need 3+ states&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The checkbox is still simple. It's binary, presence-based, and does exactly one thing.&lt;/p&gt;

&lt;p&gt;The complexity comes from what we try to make it represent.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://html.spec.whatwg.org/multipage/input.html#checkbox-state-(type=checkbox)" rel="noopener noreferrer"&gt;HTML Living Standard: Checkbox State&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/indeterminate" rel="noopener noreferrer"&gt;MDN: indeterminate property&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/wai-aria-1.1/#aria-checked" rel="noopener noreferrer"&gt;WAI-ARIA: aria-checked&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/bwi/why-checkboxes-are-not-as-simple-as-they-seem-3jhf"&gt;Part 1: Why Checkboxes Are Not as Simple as They Seem&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>frontend</category>
      <category>html</category>
      <category>ui</category>
      <category>ux</category>
    </item>
    <item>
      <title>Why Checkboxes Are Not as Simple as They Seem</title>
      <dc:creator>bwi</dc:creator>
      <pubDate>Wed, 28 Jan 2026 19:06:51 +0000</pubDate>
      <link>https://forem.com/bwi/why-checkboxes-are-not-as-simple-as-they-seem-3jhf</link>
      <guid>https://forem.com/bwi/why-checkboxes-are-not-as-simple-as-they-seem-3jhf</guid>
      <description>&lt;p&gt;Checkboxes are often described as the simplest form control. They look trivial, behave predictably, and everyone thinks they understand them.&lt;/p&gt;

&lt;p&gt;Until assumptions diverge.&lt;/p&gt;

&lt;p&gt;This article is not about bugs or blame. It's about &lt;strong&gt;understanding what a checkbox actually is&lt;/strong&gt; — step by step, from plain HTML through JavaScript to modern frameworks.&lt;/p&gt;

&lt;p&gt;Because most checkbox problems don't start with broken code. They start with &lt;strong&gt;different assumptions quietly colliding&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: The HTML Foundation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What a Checkbox Actually Is
&lt;/h3&gt;

&lt;p&gt;According to UI definitions, a checkbox is a control that allows the user to make a &lt;strong&gt;binary choice&lt;/strong&gt; — essentially "on/off" in terms of selection state. Binary means: exactly two options, nothing in between.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://html.spec.whatwg.org/multipage/input.html#checkbox-state-(type=checkbox)" rel="noopener noreferrer"&gt;HTML specification&lt;/a&gt;, a checkbox exposes its state through &lt;strong&gt;checkedness&lt;/strong&gt; — a boolean indicating whether the control is checked. There is no built-in tri-state value.&lt;/p&gt;

&lt;p&gt;That's it. A checkbox answers exactly &lt;strong&gt;one question&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this option selected?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; represent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Yes vs. No as a domain answer&lt;/li&gt;
&lt;li&gt;True vs. False as business meaning&lt;/li&gt;
&lt;li&gt;Unknown, unset, or partial states&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  UI State vs. Form Submission
&lt;/h3&gt;

&lt;p&gt;As a UI control, a checkbox gives you exactly one thing: whether it is checked or not checked.&lt;/p&gt;

&lt;p&gt;But HTML checkboxes often live inside &lt;strong&gt;HTML forms&lt;/strong&gt;. And forms are not about UI state — they are about &lt;strong&gt;data transmission&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So a second concept shows up. Not a second &lt;em&gt;state&lt;/em&gt;, but a second &lt;em&gt;role&lt;/em&gt;: what data should be sent when the form is submitted?&lt;/p&gt;

&lt;h3&gt;
  
  
  Two Separate Roles: &lt;code&gt;checked&lt;/code&gt; vs. &lt;code&gt;value&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;HTML checkboxes have two completely separate concepts that are often confused:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;checked&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A boolean UI state (true / false)&lt;/li&gt;
&lt;li&gt;Represents whether the checkbox is ticked&lt;/li&gt;
&lt;li&gt;This is the actual state of the control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;value&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A string payload — think of it as a &lt;strong&gt;label that gets sent along&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Only relevant during form submission&lt;/li&gt;
&lt;li&gt;Only sent &lt;strong&gt;if&lt;/strong&gt; the checkbox is checked&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In simple terms: The checkbox decides &lt;em&gt;if&lt;/em&gt; something is sent. The &lt;code&gt;value&lt;/code&gt; decides &lt;em&gt;what&lt;/em&gt; is sent.&lt;/p&gt;

&lt;p&gt;A simple example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"newsletter"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"subscribe-me"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens on form submission? (Submitting a form simply means: sending the entered information to another system, like a server.)&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Checkbox State&lt;/th&gt;
&lt;th&gt;What Gets Sent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Checked&lt;/td&gt;
&lt;td&gt;&lt;code&gt;newsletter=subscribe-me&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unchecked&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(nothing)&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice: A checkbox &lt;strong&gt;never&lt;/strong&gt; submits &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;. It submits a string if checked, and &lt;strong&gt;nothing&lt;/strong&gt; if unchecked.&lt;/p&gt;

&lt;p&gt;This makes HTML checkboxes &lt;strong&gt;presence-based&lt;/strong&gt;, not value-based.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Takeaway
&lt;/h3&gt;

&lt;p&gt;Key takeaway:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Unchecked does not mean "No".&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Unchecked means "no participation".&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In plain HTML:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;checked&lt;/strong&gt; → a statement is made&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;unchecked&lt;/strong&gt; → no statement is made&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unchecked does &lt;strong&gt;not&lt;/strong&gt; mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;no&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It simply means: &lt;em&gt;This field does not participate in the submission.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Matters: The Consent Pattern
&lt;/h3&gt;

&lt;p&gt;Consider the classic example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I accept the terms and conditions."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is &lt;strong&gt;not&lt;/strong&gt; a Yes/No question. It's a confirmation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;checked&lt;/strong&gt; → confirmation exists&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;unchecked&lt;/strong&gt; → confirmation missing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no meaningful "No" answer. You either confirm, or you don't. This is exactly what checkboxes are designed for.&lt;/p&gt;

&lt;p&gt;If your question truly needs both "Yes" and "No" as explicit answers, a checkbox is the &lt;strong&gt;wrong control&lt;/strong&gt;. Use radio buttons or a dropdown instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Checkbox Without &lt;code&gt;name&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;What about this?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"showAdvanced"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This checkbox:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;has a &lt;code&gt;checked&lt;/code&gt; state&lt;/li&gt;
&lt;li&gt;can be toggled by the user&lt;/li&gt;
&lt;li&gt;can be read and written via JavaScript&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it &lt;strong&gt;never submits anything&lt;/strong&gt; — because only form controls with a &lt;code&gt;name&lt;/code&gt; attribute participate in form submission.&lt;/p&gt;

&lt;p&gt;This makes such checkboxes ideal for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UI-only toggles (dark mode, show/hide sections)&lt;/li&gt;
&lt;li&gt;Parent/controller checkboxes&lt;/li&gt;
&lt;li&gt;Derived or aggregated display states&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 2: Adding JavaScript
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Actually Changes?
&lt;/h3&gt;

&lt;p&gt;When JavaScript enters the picture, something important happens:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing about the checkbox itself changes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;checked&lt;/code&gt; is still a boolean&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;value&lt;/code&gt; is still a string&lt;/li&gt;
&lt;li&gt;The control is still binary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;JavaScript simply gives us &lt;strong&gt;access&lt;/strong&gt; to the existing state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Reading&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isChecked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;checkbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// always true or false&lt;/span&gt;

&lt;span class="c1"&gt;// Writing&lt;/span&gt;
&lt;span class="nx"&gt;checkbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it lets us react to changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;checkbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JavaScript does not add new semantics. It only makes the existing state &lt;strong&gt;readable and writable&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;checked&lt;/code&gt; Property
&lt;/h3&gt;

&lt;p&gt;The DOM property &lt;code&gt;checkbox.checked&lt;/code&gt; is always a boolean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Never &lt;code&gt;null&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Never &lt;code&gt;undefined&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the &lt;strong&gt;single source of truth&lt;/strong&gt; for the checkbox state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Type Coercion Pitfalls
&lt;/h3&gt;

&lt;p&gt;A common source of bugs: JavaScript will happily coerce &lt;strong&gt;any&lt;/strong&gt; value to a boolean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// undefined&lt;/span&gt;
&lt;span class="nx"&gt;checkbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// checkbox shows as unchecked&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;checkbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// checkbox shows as CHECKED! (non-empty string is truthy)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No error. No warning. The assignment just works.&lt;/p&gt;

&lt;p&gt;But meaning gets collapsed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// true (!)&lt;/span&gt;
&lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, different concepts — &lt;code&gt;false&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;, "never touched", empty string — suddenly &lt;strong&gt;look identical&lt;/strong&gt; in the UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Key Rule
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Truthy/falsy is a control-flow feature, not a domain model.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Checkbox logic must be &lt;strong&gt;explicit&lt;/strong&gt;, not coerced. Just because JavaScript doesn't throw an error doesn't mean your logic is correct.&lt;/p&gt;

&lt;h3&gt;
  
  
  JavaScript Still Doesn't Provide "No"
&lt;/h3&gt;

&lt;p&gt;Even with JavaScript:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;checked === true&lt;/code&gt; → a statement exists&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;checked === false&lt;/code&gt; → no statement exists&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;JavaScript does &lt;strong&gt;not&lt;/strong&gt; turn unchecked into an explicit "No". Unchecked still means: &lt;em&gt;This checkbox is not participating.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: Modern Applications and Frameworks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Shift: From Form Submit to Data Objects
&lt;/h3&gt;

&lt;p&gt;In modern web applications, checkboxes are rarely submitted via classic HTML forms.&lt;/p&gt;

&lt;p&gt;Instead, we typically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the checkbox state in JavaScript&lt;/li&gt;
&lt;li&gt;Map it to a true/false property in a data object&lt;/li&gt;
&lt;li&gt;Send that data object to a server&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;(Technical note: These data objects are often called "DTOs" — Data Transfer Objects — and are usually sent as JSON, a text format that computers can easily read.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nameInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;subscribed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newsletterCheckbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt;  &lt;span class="c1"&gt;// boolean&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under this very common pattern:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A checkbox is &lt;strong&gt;interpreted&lt;/strong&gt; as a boolean value.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a reasonable and pragmatic choice. But it's still an &lt;strong&gt;interpretation&lt;/strong&gt;, not an inherent property of the checkbox.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Frameworks Handle It
&lt;/h3&gt;

&lt;p&gt;Frameworks like Angular, React, or Vue build on this boolean assumption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Angular&lt;/strong&gt; has a dedicated &lt;code&gt;CheckboxControlValueAccessor&lt;/code&gt; that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reads &lt;code&gt;event.target.checked&lt;/code&gt; (boolean)&lt;/li&gt;
&lt;li&gt;Writes directly to the &lt;code&gt;checked&lt;/code&gt; property&lt;/li&gt;
&lt;li&gt;Ignores the HTML &lt;code&gt;value&lt;/code&gt; attribute for single-checkbox boolean mapping&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes checkbox handling &lt;em&gt;feel&lt;/em&gt; straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Angular Reactive Forms&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormGroup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;newsletter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// boolean&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The framework abstracts away the HTML details and gives you a clean boolean interface.&lt;/p&gt;

&lt;p&gt;This abstraction is convenient, but it can hide mismatches between UI state and domain meaning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; Come From
&lt;/h3&gt;

&lt;p&gt;If the checkbox is always &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;, where do &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; come from?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not from the checkbox itself.&lt;/strong&gt; They come from:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initialization&lt;/strong&gt; — You create a form control without a default value&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mapping/Serialization&lt;/strong&gt; — Your code only sets properties when they're truthy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Responses&lt;/strong&gt; — Backend sends &lt;code&gt;null&lt;/code&gt; for "not set"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In older Angular versions (before Typed Forms), a &lt;code&gt;FormControl&lt;/code&gt; without an initial value had &lt;code&gt;null&lt;/code&gt; as its value — not &lt;code&gt;undefined&lt;/code&gt;, not &lt;code&gt;false&lt;/code&gt;. This is an Angular design decision, not a checkbox behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Model Mismatch
&lt;/h3&gt;

&lt;p&gt;A common mismatch: four different systems have four different models:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTML&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Presence-based (checked = participates)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Type coercion (everything becomes boolean)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Framework&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;State container (FormControl has a value)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Domain/Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Business meaning (true/false/null/unknown)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each model is &lt;strong&gt;correct within its context&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Problems arise when these models collide without explicit translation layers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4: The Perspective Problem
&lt;/h2&gt;

&lt;h3&gt;
  
  
  No Bugs, Just Colliding Assumptions
&lt;/h3&gt;

&lt;p&gt;In practice:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;There is no bug here.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;There are just multiple correct assumptions that don't align.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;HTML behaves correctly&lt;/li&gt;
&lt;li&gt;JavaScript behaves correctly&lt;/li&gt;
&lt;li&gt;Angular Forms behave correctly&lt;/li&gt;
&lt;li&gt;Developers act reasonably&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system as a whole becomes inconsistent — not the individual parts.&lt;/p&gt;

&lt;h3&gt;
  
  
  When Perspectives Collide
&lt;/h3&gt;

&lt;p&gt;If you only know one layer, everyone else seems to be doing it wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Know C#/Backend?&lt;/strong&gt; → "Why doesn't the UI send &lt;code&gt;false&lt;/code&gt;?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know HTML?&lt;/strong&gt; → "Unchecked means nothing gets sent, obviously."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know JavaScript?&lt;/strong&gt; → "&lt;code&gt;!!value&lt;/code&gt; is completely normal."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know Angular?&lt;/strong&gt; → "The FormControl has &lt;code&gt;null&lt;/code&gt;, what's the problem?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each perspective is &lt;strong&gt;internally logical&lt;/strong&gt;. But each also blinds you to the others.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Real Takeaway
&lt;/h3&gt;

&lt;p&gt;A checkbox is not complicated. But it's also not magical.&lt;/p&gt;

&lt;p&gt;It provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A binary UI state&lt;/li&gt;
&lt;li&gt;Presence-based semantics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else — booleans, DTOs, defaults, workflows, &lt;code&gt;null&lt;/code&gt; handling — is an &lt;strong&gt;assumption we add on top&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Problems arise when different parts of a system operate under different assumptions, without ever making them explicit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What You Get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTML&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Binary control, presence-based submission&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read/write access, implicit type coercion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frameworks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Boolean abstraction, state management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Your Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Whatever meaning you assign&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The checkbox did exactly what it was designed to do.&lt;/p&gt;

&lt;p&gt;The question is: &lt;strong&gt;Did everyone agree on what that means?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://html.spec.whatwg.org/multipage/input.html#checkbox-state-(type=checkbox)" rel="noopener noreferrer"&gt;HTML Living Standard: Checkbox State&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox" rel="noopener noreferrer"&gt;MDN: input type="checkbox"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/indeterminate" rel="noopener noreferrer"&gt;MDN: indeterminate property&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article is Part 1 of a series on form controls. Next: "&lt;a href="https://dev.to/bwi/indeterminate-is-not-a-value-1omd"&gt;Indeterminate Is Not a Value&lt;/a&gt;" — covering indeterminate states, participation patterns, and alternatives when you need more than two business states.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>html</category>
      <category>ui</category>
      <category>ux</category>
    </item>
    <item>
      <title>Frontend – Temporal, APIs, and DateTimePickers That Don't Lie</title>
      <dc:creator>bwi</dc:creator>
      <pubDate>Thu, 22 Jan 2026 22:10:07 +0000</pubDate>
      <link>https://forem.com/bwi/frontend-temporal-apis-and-datetimepickers-that-dont-lie-6dn</link>
      <guid>https://forem.com/bwi/frontend-temporal-apis-and-datetimepickers-that-dont-lie-6dn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 8 of 8&lt;/strong&gt; in the series &lt;a href="https://dev.to/bwi/time-in-software-done-right-3mjb"&gt;Time in Software, Done Right&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;You've modeled time correctly on the backend. You've stored it properly in the database. Now you need to handle it in the browser — where users pick dates and times, and where your API sends data back and forth.&lt;/p&gt;

&lt;p&gt;JavaScript's &lt;code&gt;Date&lt;/code&gt; object has been the source of countless bugs. The Temporal API finally gives us proper types. But even with good types, you still need to think about what your DateTimePicker is actually asking users to select, and how to send that data across the wire.&lt;/p&gt;

&lt;p&gt;This article covers the Temporal API, API contract design, and the principles behind DateTimePickers that don't mislead users.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why JavaScript's Date Is Problematic
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Date&lt;/code&gt; object has been with us since 1995. It has... issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Months are 0-indexed (January = 0)&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// June 5th, not May 5th&lt;/span&gt;

&lt;span class="c1"&gt;// Parsing is inconsistent across browsers&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-06-05&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// Midnight UTC? Midnight local? Depends on browser.&lt;/span&gt;

&lt;span class="c1"&gt;// No timezone support beyond local and UTC&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTimezoneOffset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// Minutes offset, but which zone? You don't know.&lt;/span&gt;

&lt;span class="c1"&gt;// Mutable (a constant footgun)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setMonth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Mutates in place&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no &lt;code&gt;LocalDate&lt;/code&gt;, no &lt;code&gt;LocalDateTime&lt;/code&gt;, no &lt;code&gt;ZonedDateTime&lt;/code&gt;. Just one type that tries to do everything and does none of it well.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Temporal API
&lt;/h2&gt;

&lt;p&gt;The Temporal API is the modern replacement for &lt;code&gt;Date&lt;/code&gt;. It's currently at Stage 3 — the final candidate stage before standardization — and requires a polyfill in most browsers (e.g., &lt;code&gt;@js-temporal/polyfill&lt;/code&gt;). Browser support is coming, but for now, plan on using the polyfill.&lt;/p&gt;

&lt;h3&gt;
  
  
  Type Mapping
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Temporal Type&lt;/th&gt;
&lt;th&gt;NodaTime Equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Physical moment&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Temporal.Instant&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Instant&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calendar date&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Temporal.PlainDate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LocalDate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wall clock time&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Temporal.PlainTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LocalTime&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Date + time (no zone)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Temporal.PlainDateTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LocalDateTime&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IANA timezone&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Temporal.TimeZone&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DateTimeZone&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full context&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Temporal.ZonedDateTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ZonedDateTime&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The names differ (&lt;code&gt;Plain&lt;/code&gt; vs &lt;code&gt;Local&lt;/code&gt;), but the concepts are identical. If you've understood NodaTime, you already understand Temporal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Basic Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Just a date&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PlainDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-06-05&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PlainDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Months are 1-indexed!&lt;/span&gt;

&lt;span class="c1"&gt;// Just a time&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PlainTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10:00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Date and time (no zone)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PlainDateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-06-05T10:00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// With timezone&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TimeZone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Europe/Vienna&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zoned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toZonedDateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Get the instant&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;instant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;zoned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toInstant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Converting Between Types
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ZonedDateTime → components&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zoned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ZonedDateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-06-05T10:00[Europe/Vienna]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;zoned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toPlainDateTime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// PlainDateTime&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;instant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;zoned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toInstant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// Instant&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tzId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;zoned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeZoneId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;// "Europe/Vienna"&lt;/span&gt;

&lt;span class="c1"&gt;// Instant → ZonedDateTime (for display)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;instant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-06-05T08:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inVienna&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;instant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toZonedDateTimeISO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Europe/Vienna&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inLondon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;instant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toZonedDateTimeISO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Europe/London&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  DST Handling
&lt;/h3&gt;

&lt;p&gt;Temporal handles DST ambiguities explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Time that doesn't exist (spring forward gap)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PlainDateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-03-29T02:30&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TimeZone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Europe/Vienna&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Default ("compatible"): shifts forward for gaps, picks earlier occurrence for overlaps&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zoned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toZonedDateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;disambiguation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;compatible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zoned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toZonedDateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;disambiguation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reject&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;      &lt;span class="c1"&gt;// Throws&lt;/span&gt;

&lt;span class="c1"&gt;// Time that exists twice (fall back overlap)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PlainDateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-10-25T02:30&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zoned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toZonedDateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;disambiguation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;earlier&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;  &lt;span class="c1"&gt;// First occurrence&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zoned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toZonedDateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;disambiguation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;later&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;    &lt;span class="c1"&gt;// Second occurrence&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  API Contracts: Sending Time Across the Wire
&lt;/h2&gt;

&lt;p&gt;When your frontend talks to your backend, you need a clear contract for time values. There are several approaches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: ISO Strings (Simple)
&lt;/h3&gt;

&lt;p&gt;For instants, use ISO 8601 with Z suffix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-05T08:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unambiguous. Both sides parse it the same way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Structured Object (Recommended for User Intent)
&lt;/h3&gt;

&lt;p&gt;For human-scheduled times, send the components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"appointment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"localStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-05T10:00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timeZoneId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Europe/Vienna"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backend receives exactly what the user chose. It can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validate the timezone ID&lt;/li&gt;
&lt;li&gt;Handle DST ambiguities with domain-specific logic&lt;/li&gt;
&lt;li&gt;Compute the instant&lt;/li&gt;
&lt;li&gt;Store all three values&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Option 3: ZonedDateTime String
&lt;/h3&gt;

&lt;p&gt;Temporal and some APIs support bracketed timezone notation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"startsAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-05T10:00:00[Europe/Vienna]"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is compact and unambiguous, but not all JSON parsers handle it natively. You'll need custom parsing.&lt;/p&gt;

&lt;h3&gt;
  
  
  What NOT to Do
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;DON'T:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Ambiguous&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;time&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"startsAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-05T10:00:00"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;What&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;timezone?&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;DON'T:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Offset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;without&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;timezone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ID&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"startsAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-05T10:00:00+02:00"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Which&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;zone?&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;DON'T:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Unix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;timestamp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;user-scheduled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;events&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"startsAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1780758000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Lost&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;user's&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;intent&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  TypeScript Interfaces
&lt;/h2&gt;

&lt;p&gt;Define clear types for your API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// For instants (logs, events, timestamps)&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AuditEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;occurredAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// ISO 8601 with Z: "2026-06-05T08:00:00Z"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// For user-scheduled times&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ScheduledTime&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// ISO 8601 without offset: "2026-06-05T10:00:00"&lt;/span&gt;
  &lt;span class="nl"&gt;timeZoneId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// IANA zone: "Europe/Vienna"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Appointment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ScheduledTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ScheduledTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// For date-only values&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Person&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;dateOfBirth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// ISO 8601 date: "1990-03-15"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  DateTimePicker Design Principles
&lt;/h2&gt;

&lt;p&gt;A DateTimePicker is a UI component that lets users select a date, time, or both. But "picking a time" isn't as simple as it sounds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 1: Know What You're Asking For
&lt;/h3&gt;

&lt;p&gt;Before building (or choosing) a picker, decide:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What does the user select?&lt;/th&gt;
&lt;th&gt;What do you send to the backend?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Just a date&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PlainDate&lt;/code&gt; → &lt;code&gt;"2026-06-05"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Just a time&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PlainTime&lt;/code&gt; → &lt;code&gt;"10:00"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Date and time&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PlainDateTime&lt;/code&gt; → &lt;code&gt;"2026-06-05T10:00"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Date, time, and timezone&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ZonedDateTime&lt;/code&gt; → &lt;code&gt;{ local, timeZoneId }&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most pickers handle the first three. The fourth requires explicit timezone UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 2: Timezone Display — When and How
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Show the timezone when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users in different timezones use the system&lt;/li&gt;
&lt;li&gt;The selected timezone might differ from the user's local timezone&lt;/li&gt;
&lt;li&gt;The business operates across timezones&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hide the timezone when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All users are in the same timezone&lt;/li&gt;
&lt;li&gt;The context is unambiguous (e.g., "your local time")&lt;/li&gt;
&lt;li&gt;Showing it would cause confusion without adding clarity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How to show it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Display the current timezone near the picker: "Vienna (UTC+2)"&lt;/li&gt;
&lt;li&gt;Allow changing it only if the user might need a different zone&lt;/li&gt;
&lt;li&gt;Don't default to UTC — default to the user's timezone or the organization's timezone&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Principle 3: Handle DST Gaps and Overlaps
&lt;/h3&gt;

&lt;p&gt;When the user picks a time that falls in a DST transition:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gap (time doesn't exist):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Option A: Prevent selection (disable those times in the picker)&lt;/li&gt;
&lt;li&gt;Option B: Accept and adjust (shift forward), but inform the user&lt;/li&gt;
&lt;li&gt;Option C: Show a warning and ask the user to choose a different time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Overlap (time exists twice):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Option A: Ask which one they mean (before DST change or after)&lt;/li&gt;
&lt;li&gt;Option B: Pick one automatically and note it&lt;/li&gt;
&lt;li&gt;Option C: Ignore it (acceptable for many use cases)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The right choice depends on your domain. A medical appointment might need explicit handling; a casual reminder might not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 4: Don't Lie About What's Stored
&lt;/h3&gt;

&lt;p&gt;If your backend stores &lt;code&gt;local + timeZoneId&lt;/code&gt;, your picker should collect exactly that. Don't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Show a local picker but send UTC (user sees 10:00, backend gets 08:00)&lt;/li&gt;
&lt;li&gt;Show UTC but let users think it's local&lt;/li&gt;
&lt;li&gt;Convert silently and hope nobody notices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The picker's display should match what gets stored.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 5: Consider the Editing Experience
&lt;/h3&gt;

&lt;p&gt;When users edit an existing time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Show them what they originally entered (the local time)&lt;/li&gt;
&lt;li&gt;Don't show a converted UTC value&lt;/li&gt;
&lt;li&gt;If the timezone changed since creation, decide: show original zone or current zone?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Principle 6: Validation Belongs on Both Ends
&lt;/h3&gt;

&lt;p&gt;The frontend picker should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prevent obviously invalid dates (February 30th)&lt;/li&gt;
&lt;li&gt;Warn about DST issues if relevant&lt;/li&gt;
&lt;li&gt;Send well-formed data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The backend should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validate the timezone ID is real&lt;/li&gt;
&lt;li&gt;Handle DST ambiguities according to business rules&lt;/li&gt;
&lt;li&gt;Never trust that the frontend did it right&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  A Minimal Picker Contract
&lt;/h2&gt;

&lt;p&gt;For a DateTimePicker that collects a scheduled time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input (initial value):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DateTimePickerValue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// "2026-06-05T10:00"&lt;/span&gt;
  &lt;span class="nl"&gt;timeZoneId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// "Europe/Vienna"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output (on change):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DateTimePickerValue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// "2026-06-05T14:30"&lt;/span&gt;
  &lt;span class="nl"&gt;timeZoneId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// "Europe/Vienna"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The picker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Displays date and time inputs&lt;/li&gt;
&lt;li&gt;Optionally displays or allows changing the timezone&lt;/li&gt;
&lt;li&gt;Emits the combined value on change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The parent component:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Receives the structured value&lt;/li&gt;
&lt;li&gt;Sends it to the backend as-is&lt;/li&gt;
&lt;li&gt;Doesn't do timezone math&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Putting It Together: Frontend to Database
&lt;/h2&gt;

&lt;p&gt;Here's the full flow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. User picks a time in a DateTimePicker&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UI shows: June 5, 2026 at 10:00 AM (Vienna)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Frontend sends to API&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/appointments&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Team Standup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"local"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-05T10:00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timeZoneId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Europe/Vienna"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Backend (NodaTime) processes&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LocalDateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromDateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Local&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTimeZoneProviders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tzdb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeZoneId&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;instant&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InZoneLeniently&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToInstant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Store all three&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;appointment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Appointment&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;LocalStart&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TimeZoneId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeZoneId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;InstantUtc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;instant&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Database stores&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;appointments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time_zone_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instant_utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-06-05 10:00:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Europe/Vienna'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-05 08:00:00+00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Later: API returns to frontend&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Team Standup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"local"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-05T10:00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timeZoneId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Europe/Vienna"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"instantUtc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-05T08:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Frontend displays&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;To the organizer (Vienna): "June 5 at 10:00 AM"&lt;/li&gt;
&lt;li&gt;To a participant in London: "June 5 at 9:00 AM (10:00 AM Vienna)"&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Detecting the User's Timezone
&lt;/h2&gt;

&lt;p&gt;We've covered storing and displaying times with timezones. But how do you know what timezone the user is in?&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser Detection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userZone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;resolvedOptions&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;timeZone&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// "Europe/Vienna", "Europe/London", etc.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns an IANA timezone ID — exactly what you need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caveats
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VPNs and proxies&lt;/strong&gt; may cause the browser to report a different zone than the user expects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corporate networks&lt;/strong&gt; sometimes override timezone settings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User preference&lt;/strong&gt; might differ from their physical location (e.g., someone living in Vienna but working with a London team)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Best Practice
&lt;/h3&gt;

&lt;p&gt;Use browser detection as a &lt;strong&gt;default&lt;/strong&gt;, but let users &lt;strong&gt;confirm or change&lt;/strong&gt; it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detect the timezone automatically&lt;/li&gt;
&lt;li&gt;Show it clearly in the UI: "Your timezone: Europe/Vienna"&lt;/li&gt;
&lt;li&gt;Let users change it if needed&lt;/li&gt;
&lt;li&gt;Store their preference (per user, not per session)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Don't silently assume the detected timezone is correct. A user in Vienna might be scheduling a meeting for their London office.&lt;/p&gt;




&lt;h2&gt;
  
  
  Timezone Is Not a Locale (and Not a Language)
&lt;/h2&gt;

&lt;p&gt;Timezone, language, and locale are often treated as one setting — but they are three independent concerns.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Language (i18n)&lt;/strong&gt; controls text:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Today" vs "Heute" vs "Aujourd'hui"&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Locale (l10n)&lt;/strong&gt; controls formatting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;1,000.00&lt;/code&gt; vs &lt;code&gt;1.000,00&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MM/DD/YYYY&lt;/code&gt; vs &lt;code&gt;DD.MM.YYYY&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Timezone&lt;/strong&gt; controls &lt;em&gt;when things happen&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Europe/Vienna&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;America/New_York&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Asia/Tokyo&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;They often change together — but they are &lt;strong&gt;not coupled&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example
&lt;/h3&gt;

&lt;p&gt;A French-speaking user in New York expects French UI, French date formatting, and New York time. Inferring &lt;code&gt;Europe/Paris&lt;/code&gt; from &lt;code&gt;fr&lt;/code&gt; is wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  DateTimePicker Rule
&lt;/h3&gt;

&lt;p&gt;A DateTimePicker should not assume timezone based on language or locale.&lt;/p&gt;

&lt;p&gt;Timezone must come from explicit user choice, browser detection, or application context.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Temporal API&lt;/strong&gt; gives JavaScript proper types: &lt;code&gt;PlainDate&lt;/code&gt;, &lt;code&gt;PlainDateTime&lt;/code&gt;, &lt;code&gt;ZonedDateTime&lt;/code&gt;, &lt;code&gt;Instant&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API contracts&lt;/strong&gt; should be explicit: use ISO strings for instants, structured objects for user intent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DateTimePickers&lt;/strong&gt; need to know what they're collecting: date, time, datetime, or datetime + zone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show timezones&lt;/strong&gt; when they matter, hide them when they'd confuse&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle DST&lt;/strong&gt; explicitly — don't let invalid times slip through silently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't lie&lt;/strong&gt; about what's stored — the picker should match the backend model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate on both ends&lt;/strong&gt; — trust but verify&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This concludes the series &lt;a href="https://dev.to/bwi/time-in-software-done-right-3mjb"&gt;"Time in Software, Done Right."&lt;/a&gt; You now have a complete mental model for handling time — from concepts to code, from backend to database to frontend.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The next time someone says "just store it as UTC," you'll know when that's right, and when it's a trap.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>frontend</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>PostgreSQL – Storing Time Without Lying to Yourself</title>
      <dc:creator>bwi</dc:creator>
      <pubDate>Thu, 22 Jan 2026 22:07:23 +0000</pubDate>
      <link>https://forem.com/bwi/postgresql-storing-time-without-lying-to-yourself-jb1</link>
      <guid>https://forem.com/bwi/postgresql-storing-time-without-lying-to-yourself-jb1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 7 of 8&lt;/strong&gt; in the series &lt;a href="https://dev.to/bwi/time-in-software-done-right-3mjb"&gt;Time in Software, Done Right&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;You've modeled time correctly in your application. You have &lt;code&gt;LocalDateTime&lt;/code&gt;, &lt;code&gt;TimeZoneId&lt;/code&gt;, and &lt;code&gt;Instant&lt;/code&gt;. Now you need to persist it.&lt;/p&gt;

&lt;p&gt;PostgreSQL has excellent time support — but its type names are misleading, and the default behaviors can surprise you. This article explains what PostgreSQL actually does, which types to use, and how to avoid the common traps.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Two Timestamp Types
&lt;/h2&gt;

&lt;p&gt;PostgreSQL has two timestamp types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;timestamp without time zone&lt;/code&gt; (or just &lt;code&gt;timestamp&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;timestamp with time zone&lt;/code&gt; (or &lt;code&gt;timestamptz&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The names suggest one stores a timezone and one doesn't. &lt;strong&gt;That's not quite right.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  timestamp without time zone
&lt;/h3&gt;

&lt;p&gt;This stores exactly what you give it — a date and time with no timezone context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-06-05 10:00:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Result: 2026-06-05 10:00:00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No conversion happens. No timezone is stored. It's just a calendar value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use for:&lt;/strong&gt; &lt;code&gt;LocalDateTime&lt;/code&gt; — when you're storing what the user said, not when it happened globally.&lt;/p&gt;

&lt;h3&gt;
  
  
  timestamp with time zone
&lt;/h3&gt;

&lt;p&gt;This is where the name lies. PostgreSQL does &lt;strong&gt;not&lt;/strong&gt; store a timezone. It converts the input to UTC and stores UTC internally. On retrieval, it converts back to the session's timezone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Europe/Vienna'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tstz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-06-05 10:00:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Europe/London'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;tstz&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Result: 2026-06-05 04:00:00-04&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same row, different display — because PostgreSQL stored UTC internally and converted on output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use for:&lt;/strong&gt; &lt;code&gt;Instant&lt;/code&gt; — when you're storing a global moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  What PostgreSQL Actually Stores
&lt;/h2&gt;

&lt;p&gt;Let's be precise:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What's Stored&lt;/th&gt;
&lt;th&gt;What Happens on Insert&lt;/th&gt;
&lt;th&gt;What Happens on Select&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timestamp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Raw datetime&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timestamptz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Instant (normalized to UTC)&lt;/td&gt;
&lt;td&gt;Converts input to UTC&lt;/td&gt;
&lt;td&gt;Converts UTC to session timezone&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The critical insight: &lt;strong&gt;&lt;code&gt;timestamptz&lt;/code&gt; stores UTC, not a timezone.&lt;/strong&gt; The "with time zone" means "timezone-aware" — it participates in timezone conversions. It doesn't mean "includes a timezone."&lt;/p&gt;




&lt;h2&gt;
  
  
  The Session Timezone Trap
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;timestamptz&lt;/code&gt;, PostgreSQL uses your session's timezone for conversions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'UTC'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instant_utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-06-05 10:00:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- Stored as: 2026-06-05 10:00:00 UTC&lt;/span&gt;

&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Europe/Vienna'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instant_utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-06-05 10:00:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- Stored as: 2026-06-05 08:00:00 UTC (Vienna is UTC+2 in summer)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same literal, different stored value — because PostgreSQL assumed the input was in the session timezone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best practice:&lt;/strong&gt; Always use explicit UTC or offsets when inserting into &lt;code&gt;timestamptz&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instant_utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-06-05 10:00:00+00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;-- Explicit UTC&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instant_utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-06-05 10:00:00Z'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;-- Also UTC&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instant_utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-06-05 12:00:00+02'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;-- Explicit offset&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or set your application's session timezone to UTC and keep it there.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Recommended Schema
&lt;/h2&gt;

&lt;p&gt;For human-scheduled events (meetings, deadlines, appointments), use the pattern from earlier articles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;appointments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;              &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;           &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;-- Source of truth: what the user chose&lt;/span&gt;
    &lt;span class="n"&gt;local_start&lt;/span&gt;     &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;time_zone_id&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;-- Derived: for global queries and sorting&lt;/span&gt;
    &lt;span class="n"&gt;instant_utc&lt;/span&gt;     &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why three columns?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;local_start&lt;/code&gt; — The user said "10:00". That's the intent.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;time_zone_id&lt;/code&gt; — The user meant "Vienna". That's the context.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;instant_utc&lt;/code&gt; — For queries like "what's happening now?" and for sorting.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If timezone rules change, you recalculate &lt;code&gt;instant_utc&lt;/code&gt; from &lt;code&gt;local_start + time_zone_id&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing the Right Type for Each Concept
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;PostgreSQL Type&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Instant / UTC moment&lt;/td&gt;
&lt;td&gt;&lt;code&gt;timestamptz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Log timestamp, &lt;code&gt;created_at&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local datetime (user intent)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;timestamp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Meeting time, deadline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Date only&lt;/td&gt;
&lt;td&gt;&lt;code&gt;date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Birthday, holiday&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time only&lt;/td&gt;
&lt;td&gt;&lt;code&gt;time&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opening hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IANA timezone ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'Europe/Vienna'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Querying Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pattern A: "What's on my calendar on June 5th in Vienna?"
&lt;/h3&gt;

&lt;p&gt;Query by local date + timezone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;appointments&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;time_zone_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Europe/Vienna'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;local_start&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-05'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;local_start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="s1"&gt;'2026-06-06'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This finds all appointments that display as June 5th in Vienna, regardless of the global instant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern B: "What's happening globally in the next hour?"
&lt;/h3&gt;

&lt;p&gt;Query by instant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;appointments&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;instant_utc&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;instant_utc&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 hour'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This finds all appointments happening in the next hour, regardless of their local calendars.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern C: "What's happening at 10:00 in any timezone?"
&lt;/h3&gt;

&lt;p&gt;Query by local time (rare but sometimes needed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;appointments&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;local_start&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'10:00:00'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Indexing Strategies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For instant queries (most common)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_appointments_instant&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;appointments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instant_utc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This covers "what's happening now" and range queries across timezones.&lt;/p&gt;

&lt;h3&gt;
  
  
  For local calendar queries
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_appointments_local&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;appointments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time_zone_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local_start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This covers "what's on the calendar for this timezone" queries. The timezone comes first because you'll almost always filter by it.&lt;/p&gt;

&lt;h3&gt;
  
  
  For both
&lt;/h3&gt;

&lt;p&gt;If you query both ways heavily, create both indexes. The storage cost is usually worth it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: Using timestamptz for user intent
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- DON'T: Storing a meeting time as timestamptz&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;meetings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;starts_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-06-05 10:00:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You've lost the "10:00" intent. If timezone rules change, you can't recover it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: Using timestamp for log timestamps
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- DON'T: Storing a log timestamp without timezone&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;logs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;occurred_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-06-05 10:00:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is that UTC? Server time? You don't know. Use &lt;code&gt;timestamptz&lt;/code&gt; and be explicit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 3: Trusting session timezone
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- DON'T: Assuming session timezone is what you expect&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instant_utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-06-05 10:00:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What timezone was the session in? Be explicit: &lt;code&gt;'2026-06-05 10:00:00+00'&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 4: Storing timezone names in timestamptz
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- DON'T: Thinking this stores "Vienna"&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instant_utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-06-05 10:00:00 Europe/Vienna'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL converts to UTC immediately. The string &lt;code&gt;'Europe/Vienna'&lt;/code&gt; is gone. If you need the timezone, store it separately.&lt;/p&gt;




&lt;h2&gt;
  
  
  AT TIME ZONE: The Conversion Operator
&lt;/h2&gt;

&lt;p&gt;PostgreSQL's &lt;code&gt;AT TIME ZONE&lt;/code&gt; converts between timestamps and timezones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- timestamptz → timestamp in a specific zone&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;instant_utc&lt;/span&gt; &lt;span class="k"&gt;AT&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="s1"&gt;'Europe/Vienna'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;local_vienna&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;appointments&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- timestamp → timestamptz (interpreting as a specific zone)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;local_start&lt;/span&gt; &lt;span class="k"&gt;AT&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="n"&gt;time_zone_id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;instant&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;appointments&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful for display and for reconstructing the instant from stored local + timezone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; The behavior differs based on the input type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;timestamptz AT TIME ZONE 'X'&lt;/code&gt; → returns &lt;code&gt;timestamp&lt;/code&gt; (strips timezone, shows in X)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;timestamp AT TIME ZONE 'X'&lt;/code&gt; → returns &lt;code&gt;timestamptz&lt;/code&gt; (interprets as X, converts to UTC)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Handling Timezone Rule Changes
&lt;/h2&gt;

&lt;p&gt;When IANA updates timezone rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Past events&lt;/strong&gt;: Nothing to do. PostgreSQL's &lt;code&gt;timestamptz&lt;/code&gt; already stores UTC. Historical conversions use historical rules (if your system's tzdata is updated).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Future events&lt;/strong&gt;: Recalculate &lt;code&gt;instant_utc&lt;/code&gt; from &lt;code&gt;local_start + time_zone_id&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Recalculate instant_utc for future Vienna appointments&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;appointments&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;instant_utc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local_start&lt;/span&gt; &lt;span class="k"&gt;AT&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="n"&gt;time_zone_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;time_zone_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Europe/Vienna'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;instant_utc&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why storing &lt;code&gt;local_start + time_zone_id&lt;/code&gt; matters — you have everything needed to recalculate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Working with EF Core and Npgsql
&lt;/h2&gt;

&lt;p&gt;If you're using .NET with Npgsql, the mapping is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In your DbContext&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnConfiguring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbContextOptionsBuilder&lt;/span&gt; &lt;span class="n"&gt;optionsBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;optionsBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseNpgsql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseNodaTime&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;UseNodaTime()&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Instant&lt;/code&gt; ↔ &lt;code&gt;timestamptz&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LocalDateTime&lt;/code&gt; ↔ &lt;code&gt;timestamp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LocalDate&lt;/code&gt; ↔ &lt;code&gt;date&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LocalTime&lt;/code&gt; ↔ &lt;code&gt;time&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The types align naturally with our model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Example: Creating and Querying Appointments
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- gen_random_uuid() requires this extension&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pgcrypto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Create table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;appointments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;              &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;           &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;local_start&lt;/span&gt;     &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;time_zone_id&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;instant_utc&lt;/span&gt;     &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Create indexes&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_appointments_instant&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;appointments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instant_utc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_appointments_local&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;appointments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time_zone_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local_start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Insert an appointment (10:00 Vienna = 08:00 UTC in summer)&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;appointments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time_zone_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instant_utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'Team Standup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'2026-06-05 10:00:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'Europe/Vienna'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'2026-06-05 10:00:00'&lt;/span&gt; &lt;span class="k"&gt;AT&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="s1"&gt;'Europe/Vienna'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Query: What's on the Vienna calendar for June 5th?&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local_start&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;appointments&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;time_zone_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Europe/Vienna'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;local_start&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-05'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;local_start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="s1"&gt;'2026-06-06'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Query: What's happening globally in the next 2 hours?&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instant_utc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time_zone_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;appointments&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;instant_utc&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;instant_utc&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'2 hours'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Display in viewer's timezone&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;instant_utc&lt;/span&gt; &lt;span class="k"&gt;AT&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="s1"&gt;'Europe/London'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;starts_at_london&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;appointments&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  A Note on ORMs, Query Builders, and Event Stores
&lt;/h2&gt;

&lt;p&gt;The PostgreSQL model described here — storing &lt;code&gt;local_start&lt;/code&gt;, &lt;code&gt;time_zone_id&lt;/code&gt;, and a derived &lt;code&gt;instant_utc&lt;/code&gt; — is independent of how you access the database.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EF Core / Npgsql&lt;/strong&gt;: Works well with explicit mappings (see Article 6 for full NodaTime integration).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dapper&lt;/strong&gt;: Maps naturally to simple columns; you compute instants in application code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marten / Event Sourcing&lt;/strong&gt;: Events typically store an &lt;code&gt;Instant&lt;/code&gt; (&lt;code&gt;occurred_at&lt;/code&gt;) plus domain-specific local values when needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raw SQL&lt;/strong&gt;: The same rules apply — PostgreSQL doesn't care how the data got there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key idea is not the ORM — it's the data model.&lt;/p&gt;

&lt;p&gt;If you store human intent (&lt;code&gt;local + timezone&lt;/code&gt;) separately from physical moments (&lt;code&gt;instant&lt;/code&gt;), the approach works across tools, frameworks, and architectural styles.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;timestamp&lt;/code&gt;&lt;/strong&gt; stores raw datetime — use for &lt;code&gt;LocalDateTime&lt;/code&gt; (user intent)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;timestamptz&lt;/code&gt;&lt;/strong&gt; converts to/from UTC — use for &lt;code&gt;Instant&lt;/code&gt; (global moments)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;timestamptz&lt;/code&gt; does not store a timezone&lt;/strong&gt; — it stores UTC and converts on read&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For human-scheduled events&lt;/strong&gt;: store &lt;code&gt;local_start&lt;/code&gt; + &lt;code&gt;time_zone_id&lt;/code&gt; + &lt;code&gt;instant_utc&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be explicit&lt;/strong&gt; with timezones on insert — don't trust session settings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Index &lt;code&gt;instant_utc&lt;/code&gt;&lt;/strong&gt; for global queries, &lt;strong&gt;index &lt;code&gt;(time_zone_id, local_start)&lt;/code&gt;&lt;/strong&gt; for calendar queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When rules change&lt;/strong&gt;: recalculate &lt;code&gt;instant_utc&lt;/code&gt; from the stored local + timezone&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Next up: &lt;a href="https://dev.to/bwi/frontend-temporal-apis-and-datetimepickers-that-dont-lie-6dn"&gt;&lt;strong&gt;Frontend – Temporal, APIs, and DateTimePickers That Don't Lie&lt;/strong&gt;&lt;/a&gt; — bringing it all together in the browser.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>sql</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>.NET in Practice – Modeling Time with NodaTime</title>
      <dc:creator>bwi</dc:creator>
      <pubDate>Thu, 22 Jan 2026 22:05:54 +0000</pubDate>
      <link>https://forem.com/bwi/net-in-practice-modeling-time-with-nodatime-o6d</link>
      <guid>https://forem.com/bwi/net-in-practice-modeling-time-with-nodatime-o6d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 6 of 8&lt;/strong&gt; in the series &lt;a href="https://dev.to/bwi/time-in-software-done-right-3mjb"&gt;Time in Software, Done Right&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;You've made it through the conceptual articles. You understand the difference between instants and local times, between global and local events, between storing intent and storing math.&lt;/p&gt;

&lt;p&gt;Now let's make it real in .NET.&lt;/p&gt;

&lt;p&gt;The BCL has improved significantly — &lt;code&gt;DateOnly&lt;/code&gt; and &lt;code&gt;TimeOnly&lt;/code&gt; (since .NET 6) are solid types for dates and times. But for &lt;strong&gt;timezone-aware scheduling&lt;/strong&gt; — meetings, deadlines, appointments that need to survive DST changes — you'll want &lt;strong&gt;NodaTime&lt;/strong&gt;. It gives you the types the BCL is still missing.&lt;/p&gt;

&lt;p&gt;This article shows you how to use NodaTime to model time correctly, store it properly, and avoid the traps we've discussed throughout this series.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why DateTime Falls Short (and What the BCL Fixed)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;DateTime&lt;/code&gt; in .NET is a single type that tries to represent multiple concepts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// Local time on this machine&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// UTC instant&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Is this local? UTC? Unspecified?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem is the &lt;code&gt;Kind&lt;/code&gt; property:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DateTimeKind.Local&lt;/code&gt; — local to &lt;em&gt;this machine&lt;/em&gt; (not a specific timezone)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DateTimeKind.Utc&lt;/code&gt; — a UTC instant&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DateTimeKind.Unspecified&lt;/code&gt; — could be anything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you create &lt;code&gt;new DateTime(2026, 6, 5, 10, 0, 0)&lt;/code&gt;, the &lt;code&gt;Kind&lt;/code&gt; is &lt;code&gt;Unspecified&lt;/code&gt;. Is that 10:00 in Vienna? 10:00 in London? 10:00 UTC? The type doesn't know, and neither does your code.&lt;/p&gt;

&lt;h3&gt;
  
  
  The BCL Got Better: DateOnly and TimeOnly
&lt;/h3&gt;

&lt;p&gt;.NET 6 added two types that address part of this problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;DateOnly&lt;/span&gt; &lt;span class="n"&gt;birthday&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DateOnly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1990&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// Just a date, no time confusion&lt;/span&gt;
&lt;span class="n"&gt;TimeOnly&lt;/span&gt; &lt;span class="n"&gt;openingTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TimeOnly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;         &lt;span class="c1"&gt;// Just a time, no date confusion&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are great! If you just need a date or just a time, use them. They're in the BCL, well-supported by EF Core, and do exactly what they say.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the BCL Still Doesn't Have
&lt;/h3&gt;

&lt;p&gt;But for the full picture — especially &lt;strong&gt;timezone-aware scheduling&lt;/strong&gt; — the BCL still falls short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;Instant&lt;/code&gt; type (you use &lt;code&gt;DateTime&lt;/code&gt; with &lt;code&gt;Kind.Utc&lt;/code&gt; or &lt;code&gt;DateTimeOffset&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;LocalDateTime&lt;/code&gt; with proper semantics (you use &lt;code&gt;DateTime&lt;/code&gt; with &lt;code&gt;Kind.Unspecified&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;ZonedDateTime&lt;/code&gt; that combines local time with a timezone&lt;/li&gt;
&lt;li&gt;No first-class IANA timezone support (&lt;code&gt;TimeZoneInfo&lt;/code&gt; uses Windows zones by default)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;DateTimeOffset&lt;/code&gt; is better than &lt;code&gt;DateTime&lt;/code&gt; — it includes an offset — but as we discussed in Article 4, an offset is a snapshot, not a meaning. &lt;code&gt;+02:00&lt;/code&gt; could be Vienna in summer, Berlin in summer, Cairo, or Johannesburg. You can't tell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For simple cases&lt;/strong&gt;: &lt;code&gt;DateOnly&lt;/code&gt;, &lt;code&gt;TimeOnly&lt;/code&gt;, &lt;code&gt;DateTime&lt;/code&gt;, and &lt;code&gt;DateTimeOffset&lt;/code&gt; are fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For timezone-aware scheduling&lt;/strong&gt;: NodaTime gives you the right types for the right concepts.&lt;/p&gt;




&lt;h2&gt;
  
  
  The NodaTime Types You Need
&lt;/h2&gt;

&lt;p&gt;Here's how NodaTime maps to the concepts we've covered (and their BCL equivalents where they exist):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;NodaTime Type&lt;/th&gt;
&lt;th&gt;BCL Equivalent&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Physical moment&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Instant&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DateTime&lt;/code&gt; (UTC) / &lt;code&gt;DateTimeOffset&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Log timestamp, token expiry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calendar date&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LocalDate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DateOnly&lt;/code&gt; ✓&lt;/td&gt;
&lt;td&gt;Birthday, holiday&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wall clock time&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LocalTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TimeOnly&lt;/code&gt; ✓&lt;/td&gt;
&lt;td&gt;"Opens at 09:00"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Date + time (no zone)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LocalDateTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DateTime&lt;/code&gt; (Unspecified)&lt;/td&gt;
&lt;td&gt;User's chosen meeting time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IANA timezone&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DateTimeZone&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TimeZoneInfo&lt;/code&gt; (partial)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Europe/Vienna&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full context&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ZonedDateTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ None&lt;/td&gt;
&lt;td&gt;Meeting at 10:00 Vienna&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot with offset&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OffsetDateTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DateTimeOffset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What the clock showed at a moment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ✓ marks where the BCL type is a good choice. For &lt;code&gt;DateOnly&lt;/code&gt; and &lt;code&gt;TimeOnly&lt;/code&gt;, you can often skip NodaTime entirely.&lt;/p&gt;

&lt;p&gt;The gap is &lt;code&gt;ZonedDateTime&lt;/code&gt; — the combination of a local time and an IANA timezone that lets you handle DST correctly. That's where NodaTime shines.&lt;/p&gt;

&lt;p&gt;Let's see each in action.&lt;/p&gt;




&lt;h2&gt;
  
  
  Instant: For Physical Moments
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;Instant&lt;/code&gt; when you're recording &lt;em&gt;when something happened&lt;/em&gt; — independent of any human's calendar.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Current moment&lt;/span&gt;
&lt;span class="n"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SystemClock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCurrentInstant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// From a Unix timestamp&lt;/span&gt;
&lt;span class="n"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;fromUnix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromUnixTimeSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1735689600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// For logs, audits, event sourcing&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuditEntry&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;OccurredAt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Instant&lt;/code&gt; is unambiguous. There's no timezone to confuse, no &lt;code&gt;Kind&lt;/code&gt; property to check. It's just a point on the timeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  LocalDate, LocalTime, LocalDateTime: For Human Concepts
&lt;/h2&gt;

&lt;p&gt;These types represent calendar and clock values &lt;strong&gt;without&lt;/strong&gt; a timezone attached.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Just a date (NodaTime)&lt;/span&gt;
&lt;span class="n"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;birthday&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LocalDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1990&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Just a date (BCL - equally good!)&lt;/span&gt;
&lt;span class="n"&gt;DateOnly&lt;/span&gt; &lt;span class="n"&gt;birthdayBcl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DateOnly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1990&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Just a time (NodaTime)&lt;/span&gt;
&lt;span class="n"&gt;LocalTime&lt;/span&gt; &lt;span class="n"&gt;openingTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LocalTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Just a time (BCL - equally good!)&lt;/span&gt;
&lt;span class="n"&gt;TimeOnly&lt;/span&gt; &lt;span class="n"&gt;openingTimeBcl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TimeOnly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Date and time together (NodaTime)&lt;/span&gt;
&lt;span class="n"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;meetingTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LocalDateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For dates and times alone&lt;/strong&gt;, use whichever you prefer — &lt;code&gt;DateOnly&lt;/code&gt;/&lt;code&gt;TimeOnly&lt;/code&gt; are in the BCL and work great with EF Core.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For date+time combinations&lt;/strong&gt; that you'll later combine with a timezone, NodaTime's &lt;code&gt;LocalDateTime&lt;/code&gt; is clearer because it's part of a coherent type system that includes &lt;code&gt;ZonedDateTime&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;LocalDateTime&lt;/code&gt; of &lt;code&gt;2026-06-05T10:00&lt;/code&gt; means "June 5th at 10:00" — but it doesn't yet specify &lt;em&gt;where&lt;/em&gt;. That's intentional. You'll combine it with a timezone to get the full picture.&lt;/p&gt;




&lt;h2&gt;
  
  
  DateTimeZone: The Ruleset
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;DateTimeZone&lt;/code&gt; represents an IANA timezone — not just an offset, but the complete ruleset including DST transitions and historical changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Get a timezone by IANA ID&lt;/span&gt;
&lt;span class="n"&gt;DateTimeZone&lt;/span&gt; &lt;span class="n"&gt;vienna&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTimeZoneProviders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tzdb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Europe/Vienna"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;DateTimeZone&lt;/span&gt; &lt;span class="n"&gt;london&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTimeZoneProviders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tzdb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Europe/London"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// The provider gives you access to all IANA zones&lt;/span&gt;
&lt;span class="n"&gt;IDateTimeZoneProvider&lt;/span&gt; &lt;span class="n"&gt;tzdb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTimeZoneProviders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tzdb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DateTimeZoneProviders.Tzdb&lt;/code&gt; uses the IANA tz database, which is updated regularly with new rules. When you update NodaTime's tzdb data, your code automatically handles new DST rules.&lt;/p&gt;




&lt;h2&gt;
  
  
  ZonedDateTime: The Full Picture
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ZonedDateTime&lt;/code&gt; combines a &lt;code&gt;LocalDateTime&lt;/code&gt; with a &lt;code&gt;DateTimeZone&lt;/code&gt; — giving you everything you need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LocalDateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;DateTimeZone&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTimeZoneProviders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tzdb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Europe/Vienna"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Combine them&lt;/span&gt;
&lt;span class="n"&gt;ZonedDateTime&lt;/span&gt; &lt;span class="n"&gt;zoned&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InZoneLeniently&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Now you can get the instant&lt;/span&gt;
&lt;span class="n"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;instant&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;zoned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToInstant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Or display in different zones&lt;/span&gt;
&lt;span class="n"&gt;ZonedDateTime&lt;/span&gt; &lt;span class="n"&gt;inLondon&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;instant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTimeZoneProviders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tzdb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Europe/London"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inNewYork&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"uuuu-MM-dd HH:mm x"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CultureInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvariantCulture&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// Output: 2026-06-05 04:00 -04&lt;/span&gt;
&lt;span class="c1"&gt;// (Requires: using System.Globalization;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why "Leniently"?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;InZoneLeniently&lt;/code&gt; method handles DST edge cases automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the local time falls in a &lt;strong&gt;gap&lt;/strong&gt; (doesn't exist), it shifts forward&lt;/li&gt;
&lt;li&gt;If the local time falls in an &lt;strong&gt;overlap&lt;/strong&gt; (exists twice), it picks the earlier occurrence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For explicit control, NodaTime offers several options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Called on LocalDateTime&lt;/span&gt;
&lt;span class="n"&gt;ZonedDateTime&lt;/span&gt; &lt;span class="n"&gt;zoned&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InZoneLeniently&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// Auto-resolve gaps/overlaps&lt;/span&gt;
&lt;span class="n"&gt;ZonedDateTime&lt;/span&gt; &lt;span class="n"&gt;zoned&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InZoneStrictly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// Throws if ambiguous&lt;/span&gt;

&lt;span class="c1"&gt;// Called on DateTimeZone (same behavior, different syntax)&lt;/span&gt;
&lt;span class="n"&gt;ZonedDateTime&lt;/span&gt; &lt;span class="n"&gt;zoned&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AtLeniently&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;ZonedDateTime&lt;/span&gt; &lt;span class="n"&gt;zoned&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AtStrictly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// With custom resolver&lt;/span&gt;
&lt;span class="n"&gt;ZonedDateTime&lt;/span&gt; &lt;span class="n"&gt;zoned&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Resolvers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LenientResolver&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Pattern: Store Intent, Derive Instant
&lt;/h2&gt;

&lt;p&gt;Here's the core pattern from Article 4, implemented in NodaTime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Appointment&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Source of truth: what the user chose&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;LocalStart&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;TimeZoneId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Derived: for queries and sorting&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;InstantUtc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;RecalculateInstant&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTimeZoneProviders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tzdb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TimeZoneId&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;zoned&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LocalStart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InZoneLeniently&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;InstantUtc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;zoned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToInstant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When timezone rules change, you call &lt;code&gt;RecalculateInstant()&lt;/code&gt; on future appointments. Past appointments stay correct because IANA contains historical rules.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Example 1: Logging (Use Instant)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LogEntry&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;Timestamp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Level&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;LogEntry&lt;/span&gt; &lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;LogEntry&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Timestamp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SystemClock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCurrentInstant&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;Level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example 2: Birthday (Use LocalDate)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Person&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;DateOfBirth&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;GetAge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Period&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Between&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateOfBirth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PeriodUnits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Years&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Years&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No timezone needed — birthdays are calendar concepts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 3: Meeting (Use LocalDateTime + TimeZone)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Meeting&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;LocalStart&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;TimeZoneId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;InstantUtc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Meeting&lt;/span&gt; &lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;localStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;timeZoneId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTimeZoneProviders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tzdb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;timeZoneId&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;instant&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;localStart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InZoneLeniently&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToInstant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Meeting&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;LocalStart&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;localStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;TimeZoneId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timeZoneId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;InstantUtc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;instant&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Display in any timezone&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetDisplayTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTimeZone&lt;/span&gt; &lt;span class="n"&gt;viewerZone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;inViewerZone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;InstantUtc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewerZone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Note: uuuu is NodaTime's recommended year specifier (absolute year)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;inViewerZone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"uuuu-MM-dd HH:mm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CultureInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvariantCulture&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example 4: Deadline (Use LocalDateTime + TimeZone)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Deadline&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;LocalDeadline&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;TimeZoneId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;InstantUtc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsPastDeadline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;InstantUtc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt; &lt;span class="nf"&gt;TimeRemaining&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;InstantUtc&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  EF Core Integration
&lt;/h2&gt;

&lt;p&gt;NodaTime doesn't map to SQL types out of the box, but there are excellent packages for this.&lt;/p&gt;

&lt;h3&gt;
  
  
  For PostgreSQL: Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In your DbContext configuration&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnConfiguring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbContextOptionsBuilder&lt;/span&gt; &lt;span class="n"&gt;optionsBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;optionsBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseNpgsql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseNodaTime&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This maps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Instant&lt;/code&gt; → &lt;code&gt;timestamp with time zone&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LocalDateTime&lt;/code&gt; → &lt;code&gt;timestamp without time zone&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LocalDate&lt;/code&gt; → &lt;code&gt;date&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LocalTime&lt;/code&gt; → &lt;code&gt;time&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What about &lt;code&gt;ZonedDateTime&lt;/code&gt;?&lt;/strong&gt; There's no single PostgreSQL type for it — that's the whole point of our pattern. You decompose it into separate columns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;LocalDateTime&lt;/code&gt; → &lt;code&gt;timestamp without time zone&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TimeZoneId&lt;/code&gt; → &lt;code&gt;text&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Optionally: &lt;code&gt;Instant&lt;/code&gt; → &lt;code&gt;timestamp with time zone&lt;/code&gt; (for queries)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's how to extract the parts from a &lt;code&gt;ZonedDateTime&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;ZonedDateTime&lt;/span&gt; &lt;span class="n"&gt;zoned&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InZoneLeniently&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Decompose for storage&lt;/span&gt;
&lt;span class="n"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;localPart&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;zoned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalDateTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;timeZoneId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;zoned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// e.g. "Europe/Vienna"&lt;/span&gt;
&lt;span class="n"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;instantPart&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;zoned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToInstant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  For SQL Server: Consider Value Converters
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppointmentConfiguration&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEntityTypeConfiguration&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Appointment&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EntityTypeBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Appointment&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalStart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasConversion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToDateTimeUnspecified&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LocalDateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromDateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InstantUtc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasConversion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToDateTimeUtc&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromDateTimeUtc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeZoneId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasMaxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Sample Entity
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Appointment&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Stored as timestamp without time zone&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;LocalStart&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Stored as text/varchar&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;TimeZoneId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Stored as timestamp with time zone (for queries)&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;InstantUtc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Handling DST Transitions
&lt;/h2&gt;

&lt;p&gt;When creating appointments that might fall in DST gaps or overlaps, be explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppointmentService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ZonedDateTime&lt;/span&gt; &lt;span class="nf"&gt;ResolveLocalTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;timeZoneId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTimeZoneProviders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tzdb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;timeZoneId&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mapping&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapLocal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AtLeniently&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;        &lt;span class="c1"&gt;// Gap: shift forward to valid time&lt;/span&gt;
            &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Single&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;                &lt;span class="c1"&gt;// Normal: exactly one mapping&lt;/span&gt;
            &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;First&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;                 &lt;span class="c1"&gt;// Overlap: pick earlier occurrence&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For more control (e.g., asking the user to choose during overlaps):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ZonedDateTime&lt;/span&gt; &lt;span class="nf"&gt;ResolveWithUserChoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;timeZoneId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ZonedDateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ZonedDateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ZonedDateTime&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;overlapResolver&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTimeZoneProviders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tzdb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;timeZoneId&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mapping&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapLocal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AtLeniently&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Single&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;overlapResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;First&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Last&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Converting from DateTime
&lt;/h2&gt;

&lt;p&gt;If you have existing code using &lt;code&gt;DateTime&lt;/code&gt;, here's how to convert:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DateTime (UTC) to Instant&lt;/span&gt;
&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;dtUtc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;instant&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromDateTimeUtc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dtUtc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// DateTime (unspecified) to LocalDateTime&lt;/span&gt;
&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LocalDateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromDateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Instant to DateTime (UTC)&lt;/span&gt;
&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;backToUtc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;instant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToDateTimeUtc&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// LocalDateTime to DateTime (unspecified)&lt;/span&gt;
&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;backToUnspecified&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToDateTimeUnspecified&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing Time-Dependent Code
&lt;/h2&gt;

&lt;p&gt;Code that calls &lt;code&gt;SystemClock.Instance.GetCurrentInstant()&lt;/code&gt; directly is hard to test. You can't control "now".&lt;/p&gt;

&lt;p&gt;NodaTime solves this with &lt;code&gt;IClock&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Production: inject the real clock&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppointmentService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IClock&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsUpcoming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Appointment&lt;/span&gt; &lt;span class="n"&gt;appointment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCurrentInstant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;appointment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InstantUtc&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// In production&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AppointmentService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SystemClock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// In tests: use a fake clock&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fakeNow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromUtc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fakeClock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FakeClock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fakeNow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AppointmentService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fakeClock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Now you can test time-dependent logic deterministically&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule:&lt;/strong&gt; Never call &lt;code&gt;SystemClock.Instance&lt;/code&gt; directly in business logic. Inject &lt;code&gt;IClock&lt;/code&gt; instead. Your tests will thank you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use NodaTime&lt;/strong&gt; for anything beyond simple logging — it gives you the right types for the right concepts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Instant&lt;/code&gt;&lt;/strong&gt; for physical moments (logs, events, tokens)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LocalDate&lt;/code&gt;&lt;/strong&gt; for calendar dates (birthdays, holidays)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LocalDateTime&lt;/code&gt; + &lt;code&gt;DateTimeZone&lt;/code&gt;&lt;/strong&gt; for human-scheduled times (meetings, deadlines)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store intent&lt;/strong&gt;: &lt;code&gt;LocalDateTime&lt;/code&gt; + &lt;code&gt;TimeZoneId&lt;/code&gt; as your source of truth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Derive instant&lt;/strong&gt;: compute &lt;code&gt;InstantUtc&lt;/code&gt; for queries and sorting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle DST explicitly&lt;/strong&gt;: use &lt;code&gt;InZoneLeniently&lt;/code&gt; or check &lt;code&gt;MapLocal&lt;/code&gt; for edge cases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EF Core&lt;/strong&gt;: use &lt;code&gt;Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime&lt;/code&gt; for PostgreSQL, or value converters for other databases&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Next up: &lt;a href="https://dev.to/bwi/postgresql-storing-time-without-lying-to-yourself-jb1"&gt;&lt;strong&gt;PostgreSQL – Storing Time Without Lying to Yourself&lt;/strong&gt;&lt;/a&gt; — the database side of the equation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>softwareengineering</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Global Events, Local Events, and Recurring Rules</title>
      <dc:creator>bwi</dc:creator>
      <pubDate>Thu, 22 Jan 2026 22:03:41 +0000</pubDate>
      <link>https://forem.com/bwi/global-events-local-events-and-recurring-rules-3bhc</link>
      <guid>https://forem.com/bwi/global-events-local-events-and-recurring-rules-3bhc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 5 of 8&lt;/strong&gt; in the series &lt;a href="https://dev.to/bwi/time-in-software-done-right-3mjb"&gt;Time in Software, Done Right&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;"All stores open at 10:00."&lt;/p&gt;

&lt;p&gt;Simple requirement, right? But this sentence hides a massive ambiguity that will determine your entire data model.&lt;/p&gt;

&lt;p&gt;Do all stores open &lt;strong&gt;at the same moment worldwide&lt;/strong&gt; (a global event)?&lt;br&gt;&lt;br&gt;
Or does each store open &lt;strong&gt;when its local clock shows 10:00&lt;/strong&gt; (a local event)?&lt;/p&gt;

&lt;p&gt;Same words. Completely different meanings. Completely different storage.&lt;/p&gt;

&lt;p&gt;This article will help you spot the difference — and handle the even trickier case of &lt;strong&gt;recurring events&lt;/strong&gt;, where Daylight Saving Time (DST) creates times that don't exist and times that exist twice.&lt;/p&gt;


&lt;h2&gt;
  
  
  Global Events vs Local Events
&lt;/h2&gt;

&lt;p&gt;Let's make the distinction crystal clear.&lt;/p&gt;
&lt;h3&gt;
  
  
  Global Event: Same Moment, Different Clocks
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;global event&lt;/strong&gt; happens at one physical instant. Everyone experiences it at the same moment, but their clocks show different times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product launch&lt;/strong&gt;: "New iPhone available at 10:00 AM Pacific" — that's 7:00 PM in Vienna&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Livestream start&lt;/strong&gt;: "Keynote begins at 18:00 UTC" — same instant everywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stock market close&lt;/strong&gt;: The NYSE closes at one moment, regardless of where traders are&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server maintenance window&lt;/strong&gt;: "Downtime from 02:00-04:00 UTC"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How to store:&lt;/strong&gt; &lt;code&gt;Instant&lt;/code&gt; (UTC). Everyone converts to their local clock for display.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event_type:     global
instant_utc:    2026-06-05T17:00:00Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you query "what's happening now?", you compare against the current instant. Done.&lt;/p&gt;




&lt;h3&gt;
  
  
  Local Event: Same Clock, Different Moments
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;local event&lt;/strong&gt; happens when local clocks show a specific time. The physical moment is different for each timezone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Store opening hours&lt;/strong&gt;: "All stores open at 10:00" — means 10:00 in London, 10:00 in Vienna, 10:00 in Tokyo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TV broadcast&lt;/strong&gt;: "News at 8 PM" — 8 PM in your timezone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily standup&lt;/strong&gt;: "Team meeting at 09:00 Vienna time"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deadline&lt;/strong&gt;: "Submit by 23:59 in your local timezone"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How to store:&lt;/strong&gt; &lt;code&gt;local_start + time_zone_id&lt;/code&gt;. Each location has its own timezone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event_type:     local
local_start:    10:00
time_zone_id:   (per location)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;em&gt;its&lt;/em&gt; clock shows 10:00.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Critical Question
&lt;/h3&gt;

&lt;p&gt;When someone says "all X happen at 10:00", ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Do they all happen &lt;strong&gt;at the same moment&lt;/strong&gt; (global), or do they each happen &lt;strong&gt;when local clocks show 10:00&lt;/strong&gt; (local)?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer changes everything:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Product available at 10:00 Pacific"&lt;/td&gt;
&lt;td&gt;Global&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Instant&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Stores open at 10:00"&lt;/td&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LocalTime + TimeZone&lt;/code&gt; per location&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Meeting at 14:00 Vienna"&lt;/td&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LocalDateTime + TimeZone&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Server restart at 03:00 UTC"&lt;/td&gt;
&lt;td&gt;Global&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Instant&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Get this wrong, and your stores either all open at the wrong time, or your "global launch" happens at different moments in different regions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recurring Rules: A Different Beast
&lt;/h2&gt;

&lt;p&gt;Recurring events aren't timestamps at all. They're &lt;strong&gt;rules&lt;/strong&gt; that generate timestamps.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Every Monday at 10:00"&lt;/li&gt;
&lt;li&gt;"First Friday of each month"&lt;/li&gt;
&lt;li&gt;"Every day at 03:00"&lt;/li&gt;
&lt;li&gt;"Yearly on December 25th"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't store a list of dates. You store the &lt;strong&gt;rule&lt;/strong&gt;, and compute the occurrences when needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;recurrence_rule:    FREQ=WEEKLY;BYDAY=MO
local_time:         10:00
time_zone_id:       Europe/Vienna
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This rule, combined with the timezone, generates concrete &lt;code&gt;ZonedDateTime&lt;/code&gt; instances: Monday June 1st at 10:00 Vienna, Monday June 8th at 10:00 Vienna, and so on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Recurrence Must Be Local
&lt;/h3&gt;

&lt;p&gt;Recurring events are almost always &lt;strong&gt;local events&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;"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.&lt;/p&gt;

&lt;p&gt;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 &lt;strong&gt;09:00 Vienna time&lt;/strong&gt;. The standup just moved by an hour.&lt;/p&gt;

&lt;p&gt;Store the rule in local time. Let the timezone handle DST.&lt;/p&gt;




&lt;h2&gt;
  
  
  DST Edge Cases: The Hard Part
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Edge Case 1: The Time That Doesn't Exist
&lt;/h3&gt;

&lt;p&gt;In most of Europe, clocks spring forward on the last Sunday of March. At 02:00, clocks jump to 03:00.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The times 02:00, 02:15, 02:30, 02:45 don't exist that day.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What if you have a recurring event at 02:30 every day?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;March 28th at 02:30 Vienna — exists normally&lt;/li&gt;
&lt;li&gt;March 29th at 02:30 Vienna — &lt;strong&gt;doesn't exist&lt;/strong&gt; (clocks skip from 01:59 to 03:00)&lt;/li&gt;
&lt;li&gt;March 30th at 02:30 Vienna — exists normally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What should happen?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most systems choose one of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Skip&lt;/strong&gt; the occurrence (it's in a gap, treat it as if it didn't happen)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shift forward&lt;/strong&gt; to 03:00 (the first valid moment after the gap)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shift backward&lt;/strong&gt; to 01:59 (the last valid moment before the gap)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NodaTime calls these resolution strategies. There's no universally "correct" answer — it depends on your domain.&lt;/p&gt;

&lt;p&gt;A 02:30 AM cron job that does cleanup? Probably shift forward to 03:00.&lt;br&gt;&lt;br&gt;
A 02:30 AM medication reminder? Maybe shift backward — better early than missed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The point:&lt;/strong&gt; You need to decide, and your code needs to handle it explicitly.&lt;/p&gt;




&lt;h3&gt;
  
  
  Edge Case 2: The Time That Exists Twice
&lt;/h3&gt;

&lt;p&gt;In autumn, clocks fall back. In most of Europe, at 03:00 on the last Sunday of October, clocks jump back to 02:00.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The times 02:00 to 02:59 exist twice that day&lt;/strong&gt; — once before the change, once after.&lt;/p&gt;

&lt;p&gt;What if you have a recurring event at 02:30?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;October 25th at 02:30 Vienna — exists once&lt;/li&gt;
&lt;li&gt;October 26th at 02:30 Vienna — &lt;strong&gt;exists twice&lt;/strong&gt; (once in summer time, once in winter time)&lt;/li&gt;
&lt;li&gt;October 27th at 02:30 Vienna — exists once&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What should happen?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pick the first occurrence&lt;/strong&gt; (summer time, before clocks change)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the second occurrence&lt;/strong&gt; (winter time, after clocks change)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trigger both&lt;/strong&gt; (if it's a job that should run twice)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Again, domain-dependent. A daily backup at 02:30? Probably run once, pick whichever. A reminder? Probably the first one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cron Jobs vs Calendar Events
&lt;/h2&gt;

&lt;p&gt;Cron jobs and calendar events &lt;em&gt;look&lt;/em&gt; similar but behave differently with DST.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cron: Usually System Time (Often UTC)
&lt;/h3&gt;

&lt;p&gt;Traditional cron runs on the system clock. If your server is in UTC, a &lt;code&gt;0 2 * * *&lt;/code&gt; job runs at 02:00 UTC every day — no DST weirdness, but it drifts relative to local calendars.&lt;/p&gt;

&lt;p&gt;If your server uses local time, cron will experience DST gaps and overlaps. Most cron implementations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Skip&lt;/strong&gt; jobs that fall into a gap&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run once&lt;/strong&gt; for jobs that fall into an overlap (not twice)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But this varies by implementation. Check your system's documentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Calendar Events: User Intent
&lt;/h3&gt;

&lt;p&gt;Calendar events are about &lt;strong&gt;user intent&lt;/strong&gt;. "Remind me at 09:00 every day" means 09:00 on my wall clock, regardless of DST.&lt;/p&gt;

&lt;p&gt;If you're building a calendar or reminder system, you need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Store the rule in local time + timezone&lt;/li&gt;
&lt;li&gt;Compute occurrences by applying the rule to the timezone&lt;/li&gt;
&lt;li&gt;Handle gaps and overlaps with explicit resolution strategies&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Holidays: The Complex Recurrence Rules
&lt;/h2&gt;

&lt;p&gt;Some recurring events don't follow simple patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Easter&lt;/strong&gt;: First Sunday after the ecclesiastical full moon on or after March 21st (yes, really)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thanksgiving (US)&lt;/strong&gt;: Fourth Thursday of November&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ramadan&lt;/strong&gt;: Based on the Islamic lunar calendar — starts on a different Gregorian date each year&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chinese New Year&lt;/strong&gt;: Based on the lunisolar calendar&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For these, you typically need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A specialized library (like NodaTime's &lt;code&gt;IsoDayOfWeek&lt;/code&gt; calculations, or dedicated holiday libraries)&lt;/li&gt;
&lt;li&gt;A database of dates (for religious holidays that depend on moon sightings)&lt;/li&gt;
&lt;li&gt;Possibly user input ("Ramadan starts on X this year")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't try to compute Easter from scratch. Use a library.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Guidance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For One-Time Events
&lt;/h3&gt;

&lt;p&gt;Ask: "Same moment worldwide, or same local time?"&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same moment:&lt;/strong&gt; Store as &lt;code&gt;Instant&lt;/code&gt; (UTC)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same local time:&lt;/strong&gt; Store as &lt;code&gt;local_start + time_zone_id&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Recurring Events
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Store the rule&lt;/strong&gt;, not a list of occurrences&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store local time + timezone&lt;/strong&gt;, not UTC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide your DST resolution strategy&lt;/strong&gt; (skip, shift forward, shift backward)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document it&lt;/strong&gt; — future you will thank present you&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  For Cron Jobs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;If possible, run in UTC to avoid DST surprises&lt;/li&gt;
&lt;li&gt;If you must use local time, test what happens on DST transition days&lt;/li&gt;
&lt;li&gt;Consider whether a skipped or doubled job matters for your use case&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Global events&lt;/strong&gt; happen at one instant; store as UTC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local events&lt;/strong&gt; happen when local clocks match; store as local time + timezone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recurring rules&lt;/strong&gt; generate timestamps; store the rule, not the instances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DST creates gaps&lt;/strong&gt; (times that don't exist) and &lt;strong&gt;overlaps&lt;/strong&gt; (times that exist twice)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cron and calendars behave differently&lt;/strong&gt; with DST — know which you're building&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex holidays&lt;/strong&gt; (Easter, Ramadan) need specialized libraries or data&lt;/li&gt;
&lt;li&gt;When in doubt, ask: "Same moment, or same clock reading?"&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Next up: &lt;a href="https://dev.to/bwi/net-in-practice-modeling-time-with-nodatime-o6d"&gt;&lt;strong&gt;.NET in Practice – Modeling Time with NodaTime&lt;/strong&gt;&lt;/a&gt; — making all this theory real in code.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>database</category>
      <category>programming</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Instant vs Local – When UTC Helps and When It Hurts</title>
      <dc:creator>bwi</dc:creator>
      <pubDate>Thu, 22 Jan 2026 22:01:57 +0000</pubDate>
      <link>https://forem.com/bwi/instant-vs-local-when-utc-helps-and-when-it-hurts-5d7p</link>
      <guid>https://forem.com/bwi/instant-vs-local-when-utc-helps-and-when-it-hurts-5d7p</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 4 of 8&lt;/strong&gt; in the series &lt;a href="https://dev.to/bwi/time-in-software-done-right-3mjb"&gt;Time in Software, Done Right&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;"Just store everything in UTC."&lt;/p&gt;

&lt;p&gt;You've probably heard this advice. It sounds simple, clean, universal. And for some use cases, it's exactly right.&lt;/p&gt;

&lt;p&gt;But for others, it's a trap that will corrupt your data in ways you won't notice until it's too late.&lt;/p&gt;

&lt;p&gt;This article explains when UTC is the right choice, when it destroys meaning, and what model actually works for human-facing times.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Fundamentally Different Questions
&lt;/h2&gt;

&lt;p&gt;When you're storing a time, you need to ask yourself which question you're answering:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question A:&lt;/strong&gt; "What physical moment did this happen?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question B:&lt;/strong&gt; "What did the human mean when they said '10:00 in Vienna'?"&lt;/p&gt;

&lt;p&gt;These are not the same question. They need different storage strategies.&lt;/p&gt;




&lt;h2&gt;
  
  
  When UTC Is Perfect
&lt;/h2&gt;

&lt;p&gt;UTC (or Instant) is the right choice when you're recording &lt;strong&gt;physical moments&lt;/strong&gt; — things that happened at a specific point in time, independent of any human's calendar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use UTC for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Log entries&lt;/strong&gt; — "Request received at 2026-01-21T13:05:12Z"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit trails&lt;/strong&gt; — "User clicked 'Submit' at this instant"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token expiry&lt;/strong&gt; — "This token is valid for 3600 seconds from now"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event sourcing&lt;/strong&gt; — "This event occurred at this physical moment"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timestamps for ordering&lt;/strong&gt; — "Did A happen before or after B?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durations and intervals&lt;/strong&gt; — "How long did this take?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In all these cases, you don't care about calendars, timezones, or what the clock on someone's wall showed. You care about &lt;strong&gt;the moment itself&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;UTC is perfect for this. It's unambiguous, comparable, and never changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  When UTC Destroys Meaning
&lt;/h2&gt;

&lt;p&gt;UTC is the &lt;strong&gt;wrong&lt;/strong&gt; choice when you're storing &lt;strong&gt;human intent&lt;/strong&gt; — times that exist because a person chose them, in a specific calendar context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't use UTC alone for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Meetings&lt;/strong&gt; — "Team standup at 10:00 Vienna time"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Appointments&lt;/strong&gt; — "Doctor's appointment at 14:30"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deadlines&lt;/strong&gt; — "Submit by June 5th, 23:59 Vienna"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opening hours&lt;/strong&gt; — "Store opens at 09:00"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recurring events&lt;/strong&gt; — "Every Monday at 10:00"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Birthdays and anniversaries&lt;/strong&gt; — "Party on March 15th at 19:00"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why? Because these times have &lt;strong&gt;meaning beyond the instant&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This isn't obvious at first — many systems start this way and run into problems later.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem with "Just Convert to UTC"
&lt;/h2&gt;

&lt;p&gt;Let's say a user in Vienna schedules a meeting for June 5th at 10:00.&lt;/p&gt;

&lt;p&gt;If you convert immediately to UTC and store only that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stored: 2026-06-05T08:00:00Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You've lost information. You no longer know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;That the user meant "10:00 in Vienna"&lt;/li&gt;
&lt;li&gt;That Vienna was in CEST (Central European Summer Time) at that moment&lt;/li&gt;
&lt;li&gt;What should happen if timezone rules change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now imagine: the EU abolishes Daylight Saving Time in 2027. Vienna stays on permanent standard time (UTC+1 instead of UTC+2 in summer).&lt;/p&gt;

&lt;p&gt;Your stored UTC value &lt;code&gt;08:00Z&lt;/code&gt; now displays as &lt;strong&gt;09:00 Vienna time&lt;/strong&gt; — not 10:00!&lt;/p&gt;

&lt;p&gt;The user said "10:00". The meeting should still be at 10:00. But you stored the math, not the meaning.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Offsets Don't Help
&lt;/h2&gt;

&lt;p&gt;"But I stored &lt;code&gt;2026-06-05T10:00:00+02:00&lt;/code&gt; — that includes the offset!"&lt;/p&gt;

&lt;p&gt;Yes, but an offset is a &lt;strong&gt;snapshot&lt;/strong&gt;, not a &lt;strong&gt;meaning&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;+02:00&lt;/code&gt; could be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vienna in summer&lt;/li&gt;
&lt;li&gt;Berlin in summer&lt;/li&gt;
&lt;li&gt;Cairo&lt;/li&gt;
&lt;li&gt;Johannesburg&lt;/li&gt;
&lt;li&gt;Kaliningrad&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can't tell which one. And more importantly: if the rules change, you can't recalculate.&lt;/p&gt;

&lt;p&gt;An offset tells you what the clock showed at the moment of storage. It doesn't tell you what the user &lt;strong&gt;meant&lt;/strong&gt;, or what the correct time should be in the future.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Correct Model: Store Intent, Derive Math
&lt;/h2&gt;

&lt;p&gt;Here's the model that actually works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source of truth:    local + timeZoneId
Derived value:      instantUtc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a meeting at 10:00 Vienna:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local:        2026-06-05T10:00
timeZoneId:   Europe/Vienna
instantUtc:   2026-06-05T08:00:00Z  (derived)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What you store as truth:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The local datetime the user chose (10:00)&lt;/li&gt;
&lt;li&gt;The IANA timezone (Europe/Vienna)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What you derive for queries:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The UTC instant (computed from local + zone)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why IANA Timezones Matter
&lt;/h2&gt;

&lt;p&gt;An IANA timezone like &lt;code&gt;Europe/Vienna&lt;/code&gt; is not just an offset. It's a &lt;strong&gt;ruleset&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Historical offsets (what was the offset in 1980?)&lt;/li&gt;
&lt;li&gt;Current offset&lt;/li&gt;
&lt;li&gt;DST transitions (when does the clock change?)&lt;/li&gt;
&lt;li&gt;Future rules (as currently known)&lt;/li&gt;
&lt;li&gt;Political changes (when a country changes its timezone policy)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you store &lt;code&gt;Europe/Vienna&lt;/code&gt;, you're saying: "Apply whatever rules Vienna has — past, present, or future."&lt;/p&gt;

&lt;p&gt;This is why the model survives rule changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Happens When Rules Change
&lt;/h2&gt;

&lt;p&gt;Timezone rules change more often than you'd think:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Russia&lt;/strong&gt; abolished DST in 2011&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mexico&lt;/strong&gt; abolished DST in 2022&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EU&lt;/strong&gt; has been discussing abolishing DST for years&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Morocco&lt;/strong&gt; changes its offset for Ramadan every year&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Egypt&lt;/strong&gt; — the chaos champion: introduced DST in 1940, suspended it in 2011, reintroduced it in 2014, suspended it &lt;em&gt;during Ramadan&lt;/em&gt; that same year, abolished it in 2015, restored it in 2023&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the correct model:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For past events:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The IANA database contains historical rules&lt;/li&gt;
&lt;li&gt;Your &lt;code&gt;instantUtc&lt;/code&gt; was correct at the time and stays correct&lt;/li&gt;
&lt;li&gt;Recalculating gives the same result (safe, idempotent)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For future events:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When rules change, you &lt;strong&gt;recalculate&lt;/strong&gt; &lt;code&gt;instantUtc&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The human intent (10:00 Vienna) stays the same&lt;/li&gt;
&lt;li&gt;The physical moment shifts to match the new rules&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  When Do You Actually Recalculate?
&lt;/h2&gt;

&lt;p&gt;So &lt;code&gt;instantUtc&lt;/code&gt; "can always be recalculated" — but when does that actually happen?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trigger: Your IANA timezone database gets updated.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The IANA maintains the &lt;a href="https://www.iana.org/time-zones" rel="noopener noreferrer"&gt;tz database&lt;/a&gt;, which gets updated several times a year. When a country announces a DST change or offset shift, the database is updated, and eventually that update reaches your system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your OS ships a tzdata update&lt;/li&gt;
&lt;li&gt;NodaTime releases a new tzdb version&lt;/li&gt;
&lt;li&gt;Your runtime (JVM, .NET, Node) updates its bundled data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What to do when this happens:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Past events&lt;/strong&gt;: Nothing. Recalculating them is safe — the result will be identical because historical rules don't change.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Future events&lt;/strong&gt;: Recalculate &lt;code&gt;instantUtc&lt;/code&gt; for any event whose timezone was affected.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;How to implement this:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Batch job&lt;/strong&gt;: When you update your tz data, run a migration that recalculates &lt;code&gt;instantUtc&lt;/code&gt; for future events in affected zones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lazy recalculation&lt;/strong&gt;: Recompute &lt;code&gt;instantUtc&lt;/code&gt; on read, and store the tz database version used. If it's stale, recalculate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid&lt;/strong&gt;: Batch for known changes, lazy as a safety net.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most applications can get away with a simple batch job that runs after tz updates. If you're caching &lt;code&gt;instantUtc&lt;/code&gt; in a search index or external system, remember to update those too.&lt;/p&gt;

&lt;p&gt;The good news: this is rare (a few times a year, usually affecting only a handful of zones), and if you've stored &lt;code&gt;local + timeZoneId&lt;/code&gt;, you have everything you need to recalculate correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Two Query Patterns
&lt;/h2&gt;

&lt;p&gt;This model supports both ways of querying time:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern A: "What's on my calendar on June 5th in Vienna?"
&lt;/h3&gt;

&lt;p&gt;Query by local datetime + timezone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;time_zone_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Europe/Vienna'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;local_start&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-05'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;local_start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="s1"&gt;'2026-06-06'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This finds everything on that calendar day, regardless of the global instant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern B: "What's happening globally at this moment?"
&lt;/h3&gt;

&lt;p&gt;Query by instant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;instant_utc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-05T08:00:00Z'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This finds everything happening at that physical moment, regardless of local calendars.&lt;/p&gt;

&lt;p&gt;Both queries are valid. Both are useful. The model supports both.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Many Columns Do You Actually Need?
&lt;/h2&gt;

&lt;p&gt;Not every time field needs the same treatment. It depends on what you're storing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instant-only fields&lt;/strong&gt; (logs, audits, &lt;code&gt;occurredAt&lt;/code&gt;):&lt;br&gt;
→ &lt;strong&gt;1 column&lt;/strong&gt; — &lt;code&gt;timestamp with time zone&lt;/code&gt; or &lt;code&gt;Instant&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Date-only fields&lt;/strong&gt; (birthday, holiday):&lt;br&gt;
→ &lt;strong&gt;1 column&lt;/strong&gt; — &lt;code&gt;date&lt;/code&gt; or &lt;code&gt;LocalDate&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human-scheduled time&lt;/strong&gt; (appointment, meeting, deadline):&lt;br&gt;
→ &lt;strong&gt;2 columns minimum&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;local_start&lt;/code&gt; (&lt;code&gt;timestamp without time zone&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;time_zone_id&lt;/code&gt; (&lt;code&gt;text&lt;/code&gt; — IANA zone)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Optional 3rd column&lt;/strong&gt;: &lt;code&gt;instant_utc&lt;/code&gt; as a cached/derived value — only if you need efficient global queries.&lt;/p&gt;


&lt;h2&gt;
  
  
  A Practical Example
&lt;/h2&gt;

&lt;p&gt;For one human-facing timestamp (like an appointment start), the robust representation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Example using PostgreSQL types (concepts apply to any database)&lt;/span&gt;
&lt;span class="n"&gt;local_start&lt;/span&gt;    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;without&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;time_zone_id&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- IANA zone, e.g. 'Europe/Vienna'&lt;/span&gt;

&lt;span class="c1"&gt;-- Optional: cached for fast global queries/sorting&lt;/span&gt;
&lt;span class="n"&gt;instant_utc&lt;/span&gt;    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact types vary by database (&lt;code&gt;datetime&lt;/code&gt; in SQL Server, &lt;code&gt;DATETIME&lt;/code&gt; in MySQL, etc.), but the pattern is the same: &lt;strong&gt;local datetime + timezone ID + optional UTC instant&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When do you need &lt;code&gt;instant_utc&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Indexing and range queries across timezones&lt;/li&gt;
&lt;li&gt;Sorting events globally&lt;/li&gt;
&lt;li&gt;"What's happening right now?" queries&lt;/li&gt;
&lt;li&gt;Avoiding recalculation in hot paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you never query globally, you can skip it and compute on read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In practice&lt;/strong&gt;, many teams wrap these two (or three) values into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An app-level value object (&lt;code&gt;ZonedDateTime&lt;/code&gt;, a custom DTO)&lt;/li&gt;
&lt;li&gt;A composite database type&lt;/li&gt;
&lt;li&gt;A domain type with constraints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The logical model stays the same — you're just packaging it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mantra
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Store intent, not math.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;local + timeZoneId&lt;/code&gt; = what the human meant&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;instantUtc&lt;/code&gt; = what the computer needs for calculations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The human intent is the source of truth. The UTC instant is derived, cached, and can always be recalculated.&lt;/p&gt;

&lt;p&gt;If you store only UTC, you've thrown away the intent. You can never get it back.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;UTC is perfect for &lt;strong&gt;physical moments&lt;/strong&gt; (logs, audits, tokens)&lt;/li&gt;
&lt;li&gt;UTC alone is wrong for &lt;strong&gt;human intent&lt;/strong&gt; (meetings, deadlines, appointments)&lt;/li&gt;
&lt;li&gt;Offsets are snapshots, not meaning — they can't survive rule changes&lt;/li&gt;
&lt;li&gt;The correct model: &lt;strong&gt;local + IANA timezone&lt;/strong&gt; as truth, &lt;strong&gt;UTC instant&lt;/strong&gt; as derived&lt;/li&gt;
&lt;li&gt;IANA timezones contain rules, not just offsets — they handle past and future&lt;/li&gt;
&lt;li&gt;When rules change: past events stay correct, future events get recalculated&lt;/li&gt;
&lt;li&gt;Store intent, not math&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Next up: &lt;a href="https://dev.to/bwi/global-events-local-events-and-recurring-rules-3bhc"&gt;&lt;strong&gt;Global Events, Local Events, and Recurring Rules&lt;/strong&gt;&lt;/a&gt; — when "everyone at 10:00" means completely different things.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>database</category>
      <category>softwareengineering</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Deadlines Are Hard – Why "Submit by June 5th" Is Broken</title>
      <dc:creator>bwi</dc:creator>
      <pubDate>Thu, 22 Jan 2026 21:57:19 +0000</pubDate>
      <link>https://forem.com/bwi/deadlines-are-hard-why-submit-by-june-5th-is-broken-1gl4</link>
      <guid>https://forem.com/bwi/deadlines-are-hard-why-submit-by-june-5th-is-broken-1gl4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 3 of 8&lt;/strong&gt; in the series &lt;a href="https://dev.to/bwi/time-in-software-done-right-3mjb"&gt;Time in Software, Done Right&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;"Submit by June 5th."&lt;/p&gt;

&lt;p&gt;Sounds clear, right? Everyone knows what June 5th means.&lt;/p&gt;

&lt;p&gt;Except... they don't. And if you build a system around this requirement, you're setting up your users to fail.&lt;/p&gt;

&lt;p&gt;This article is about &lt;strong&gt;deadline fairness&lt;/strong&gt; — and why most deadline definitions are technically broken.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem in One Sentence
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;If a deadline is defined in a way that users cannot objectively act correctly, the problem is not the user — it's the deadline.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A user shouldn't need to guess what timezone the server is in. They shouldn't need to submit "just to be safe" hours before they think the deadline is. They shouldn't lose because of ambiguity they can't control.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Missing from "June 5th"?
&lt;/h2&gt;

&lt;p&gt;When someone says "the deadline is June 5th", what do they actually mean?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;June 5th at 00:00? (start of day)&lt;/li&gt;
&lt;li&gt;June 5th at 23:59? (end of day)&lt;/li&gt;
&lt;li&gt;June 5th at 17:00? (end of business hours)&lt;/li&gt;
&lt;li&gt;Some other time?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And more importantly: &lt;strong&gt;in which timezone?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user's timezone?&lt;/li&gt;
&lt;li&gt;The server's timezone?&lt;/li&gt;
&lt;li&gt;The organization's timezone?&lt;/li&gt;
&lt;li&gt;UTC?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"June 5th" contains none of this information. It's a &lt;strong&gt;Local Date&lt;/strong&gt; — and as we established in &lt;a href="https://dev.to/bwi/why-a-date-is-not-a-point-in-time-ad8"&gt;Article 1&lt;/a&gt;, a Local Date is not a moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Three Timezones in Every Deadline
&lt;/h2&gt;

&lt;p&gt;When a deadline involves a user and a server, there are (at least) three timezones in play:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. User Time
&lt;/h3&gt;

&lt;p&gt;Where the user physically is. A user in Sydney sees their clock, their calendar, their "June 5th".&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Server Time
&lt;/h3&gt;

&lt;p&gt;Where the server runs. AWS &lt;code&gt;us-east-1&lt;/code&gt;, Azure &lt;code&gt;westeurope&lt;/code&gt;, a data center somewhere. The server has its own clock.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Organization Time
&lt;/h3&gt;

&lt;p&gt;The business context. A company headquartered in Vienna might define all deadlines in &lt;code&gt;Europe/Vienna&lt;/code&gt;, regardless of where users or servers are.&lt;/p&gt;

&lt;p&gt;When someone says "June 5th", which of these three do they mean?&lt;/p&gt;

&lt;p&gt;Usually: &lt;strong&gt;nobody knows&lt;/strong&gt;. And that's the problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Real Scenario
&lt;/h2&gt;

&lt;p&gt;Let's trace through what happens with an ambiguous deadline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Business requirement:&lt;/strong&gt; "Users must submit by June 5th"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer interprets:&lt;/strong&gt; "End of day UTC" → stores &lt;code&gt;2026-06-05T23:59:59Z&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend displays:&lt;/strong&gt; "Due: June 5th" (no time, no timezone shown)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User in New York:&lt;/strong&gt; Sees "June 5th", submits at 11:00 PM on June 5th local time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server receives:&lt;/strong&gt; The request at &lt;code&gt;2026-06-06T03:00:00Z&lt;/code&gt; (New York is UTC-4 in June)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result:&lt;/strong&gt; Rejected — the UTC deadline has already passed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The user submitted on June 5th in their timezone. They did everything right from their perspective. But they failed because "June 5th" meant something different to the server.&lt;/p&gt;

&lt;p&gt;Now imagine this is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A tax filing&lt;/li&gt;
&lt;li&gt;A university application&lt;/li&gt;
&lt;li&gt;A legal submission&lt;/li&gt;
&lt;li&gt;A grant proposal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stakes are real. The ambiguity is unacceptable.&lt;/p&gt;




&lt;h2&gt;
  
  
  "End of Day" Doesn't Help
&lt;/h2&gt;

&lt;p&gt;Sometimes people try to fix this by saying "end of day June 5th" or "by end of business June 5th".&lt;/p&gt;

&lt;p&gt;This doesn't solve anything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;End of day&lt;/strong&gt; — whose day? What time? 23:59? 23:59:59? 23:59:59.999?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End of business&lt;/strong&gt; — whose business? What timezone? What if it's a holiday?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You've just replaced one ambiguity with another.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Only Fix: Be Explicit
&lt;/h2&gt;

&lt;p&gt;A technically correct deadline has three components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Date&lt;/strong&gt; — June 5th&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time&lt;/strong&gt; — 23:59 (or 17:00, or whatever makes sense)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timezone&lt;/strong&gt; — Europe/Vienna (or the user's timezone, explicitly stated)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example of a clear deadline:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Submit by June 5th, 2026 at 23:59 Europe/Vienna"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now there's no ambiguity. Users in any timezone can convert this to their local time. The system can enforce it precisely. Everyone knows exactly when the deadline is.&lt;/p&gt;




&lt;h2&gt;
  
  
  But What If We &lt;em&gt;Want&lt;/em&gt; User-Local Deadlines?
&lt;/h2&gt;

&lt;p&gt;Sometimes the business intent really is: "Each user has until the end of June 5th in their own timezone."&lt;/p&gt;

&lt;p&gt;That's a valid requirement! But it's a &lt;strong&gt;different&lt;/strong&gt; requirement — and it needs different handling:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Capture the user's timezone&lt;/strong&gt; (or let them choose)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store the deadline as:&lt;/strong&gt; Local Date + User's Timezone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluate per user:&lt;/strong&gt; Convert their timezone's "end of June 5th" to an Instant, compare to now&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is more complex, but it's honest. You're explicitly saying "the deadline is local to each user" rather than pretending a global deadline is local.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Systems Must Enforce
&lt;/h2&gt;

&lt;p&gt;If you're building a system that handles deadlines, here's what you need:&lt;/p&gt;

&lt;h3&gt;
  
  
  At Definition Time (when creating the deadline)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Require a &lt;strong&gt;time&lt;/strong&gt;, not just a date&lt;/li&gt;
&lt;li&gt;Require a &lt;strong&gt;timezone&lt;/strong&gt; (or default to a clearly documented one)&lt;/li&gt;
&lt;li&gt;Reject incomplete definitions — don't guess&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  At Display Time (when showing the deadline)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Show the &lt;strong&gt;full datetime with timezone&lt;/strong&gt; at least once&lt;/li&gt;
&lt;li&gt;Optionally show converted to user's local time ("June 5th 23:59 Vienna = June 6th 07:59 your time")&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  At Evaluation Time (when checking if deadline passed)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Convert everything to Instant (UTC) for comparison&lt;/li&gt;
&lt;li&gt;Use the &lt;strong&gt;stored timezone&lt;/strong&gt;, not the server's timezone&lt;/li&gt;
&lt;li&gt;Log the exact Instant of submission for disputes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  When Developers Must Push Back
&lt;/h2&gt;

&lt;p&gt;Sometimes you receive a requirement like "deadline is June 5th" and you know it's incomplete.&lt;/p&gt;

&lt;p&gt;Push back. Ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What time on June 5th?"&lt;/li&gt;
&lt;li&gt;"In which timezone?"&lt;/li&gt;
&lt;li&gt;"Should users in other timezones get the same absolute deadline, or the same local experience?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the business can't answer these questions, the deadline isn't defined yet — and you shouldn't build it.&lt;/p&gt;

&lt;p&gt;Implementing an ambiguous deadline doesn't make you fast. It makes you responsible for the bugs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fairness Test
&lt;/h2&gt;

&lt;p&gt;Here's a simple test for any deadline in your system:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can a user, acting in good faith, always determine whether they've met the deadline?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is "no" — if a user could submit on what they believe is the correct day and still fail because of timezone confusion — the deadline is broken.&lt;/p&gt;

&lt;p&gt;Fix the definition, not the user's expectations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;"June 5th" is not a deadline — it's an incomplete specification&lt;/li&gt;
&lt;li&gt;Every deadline needs: &lt;strong&gt;date + time + timezone&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;There are (at least) three timezones in play: user, server, organization&lt;/li&gt;
&lt;li&gt;"End of day" and "end of business" are just as ambiguous as bare dates&lt;/li&gt;
&lt;li&gt;User-local deadlines are valid but need explicit handling&lt;/li&gt;
&lt;li&gt;If users can't determine whether they've met the deadline, the deadline is broken — not the user&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Next up: &lt;a href="https://dev.to/bwi/instant-vs-local-when-utc-helps-and-when-it-hurts-5d7p"&gt;&lt;strong&gt;Instant vs Local — When UTC Helps and When It Hurts&lt;/strong&gt;&lt;/a&gt; — the core model for storing time correctly.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>systemdesign</category>
      <category>ux</category>
    </item>
  </channel>
</rss>
