<?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>DEV Community: Den</title>
    <description>The latest articles on DEV Community by Den (@den0011).</description>
    <link>https://dev.to/den0011</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3855195%2F9d7e6b60-0b75-4bc2-be7a-a7342ea26650.png</url>
      <title>DEV Community: Den</title>
      <link>https://dev.to/den0011</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/den0011"/>
    <language>en</language>
    <item>
      <title>What Was the Moment You Realized the Bug Wasn't Where You Thought It Was?</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Mon, 07 Sep 2026 17:27:22 +0000</pubDate>
      <link>https://dev.to/den0011/what-was-the-moment-you-realized-the-bug-wasnt-where-you-thought-it-was-2k1g</link>
      <guid>https://dev.to/den0011/what-was-the-moment-you-realized-the-bug-wasnt-where-you-thought-it-was-2k1g</guid>
      <description>&lt;p&gt;I've noticed a pattern in almost every bug I've written up here: the fix never ends up where I started looking.&lt;/p&gt;

&lt;p&gt;A validation error that looked like a schema problem turned out to be an encoding mismatch three layers upstream. A "frozen" simulator turned out to be working correctly — the bug was an unstated assumption in my evaluation algorithm, not the code executing it. A user's data disappearing turned out to be two browser tabs disagreeing about which one held the truth.&lt;/p&gt;

&lt;p&gt;Every time, I started debugging with a theory about &lt;em&gt;where&lt;/em&gt; the problem lived, and every time, the actual cause was one layer removed from that theory — not wrong code, but a wrong assumption about which code even mattered.&lt;/p&gt;

&lt;p&gt;I don't think this is unique to me. I think it might be the actual shape of most non-trivial bugs: the symptom points at a location, and the location is almost never the cause.&lt;/p&gt;

&lt;p&gt;So — what's yours? The moment you were dead certain the bug was in X, spent hours proving X was fine, and found it living quietly in Y instead. Doesn't have to be dramatic. I'm just curious whether everyone's "it wasn't where I thought" story rhymes with mine, or if there's a completely different shape to it depending on the stack.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>debugging</category>
      <category>buildlog</category>
      <category>career</category>
    </item>
    <item>
      <title>Why My React State Kept "Working" — Until Two Tabs Opened at Once</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Mon, 07 Sep 2026 08:27:57 +0000</pubDate>
      <link>https://dev.to/den0011/why-my-react-state-kept-working-until-two-tabs-opened-at-once-5952</link>
      <guid>https://dev.to/den0011/why-my-react-state-kept-working-until-two-tabs-opened-at-once-5952</guid>
      <description>&lt;p&gt;For weeks, the saved-articles feature in one of my side projects worked fine. &lt;code&gt;useState&lt;/code&gt; held the list, a save button pushed to it, a sidebar rendered it. Every manual test passed. Then a user reported that saving an article in one tab made it vanish from the sidebar in another tab — and sometimes an article they'd just unsaved would come back a few seconds later, unsaved-then-resaved, like the app couldn't make up its mind.&lt;/p&gt;

&lt;p&gt;I couldn't reproduce it for a day. I only had one tab open. That should have been my first clue.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup that looked fine
&lt;/h2&gt;

&lt;p&gt;The saved-articles state lived exactly where every React tutorial tells you to put it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useSavedArticles&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSaved&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saved-articles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;raw&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;save&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&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="nf"&gt;setSaved&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saved-articles&lt;/span&gt;&lt;span class="dl"&gt;"&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;next&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;next&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;save&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;Read from &lt;code&gt;localStorage&lt;/code&gt; on mount, write to it on every change. This is the pattern I'd copy-pasted from a dozen "persist state with localStorage" articles, and it works — for exactly one tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it breaks
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;localStorage&lt;/code&gt; is shared across every tab on the same origin. &lt;code&gt;useState&lt;/code&gt; is not. Each tab has its own React tree, its own &lt;code&gt;saved&lt;/code&gt; state, its own copy of the array sitting in memory.&lt;/p&gt;

&lt;p&gt;So the sequence that broke it looked like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tab A: user saves Article 1 → &lt;code&gt;saved = [1]&lt;/code&gt; in Tab A's memory, &lt;code&gt;localStorage&lt;/code&gt; now says &lt;code&gt;[1]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tab B was already open before that. Its &lt;code&gt;saved&lt;/code&gt; state was initialized from &lt;code&gt;localStorage&lt;/code&gt; when it &lt;em&gt;first&lt;/em&gt; mounted — which was empty. Tab B's memory still says &lt;code&gt;saved = []&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tab B: user unsaves nothing, does nothing — but then saves Article 2. Tab B computes &lt;code&gt;next&lt;/code&gt; from its own stale &lt;code&gt;prev&lt;/code&gt;, which is &lt;code&gt;[]&lt;/code&gt;, so it writes &lt;code&gt;[2]&lt;/code&gt; to &lt;code&gt;localStorage&lt;/code&gt; — silently overwriting Article 1.&lt;/li&gt;
&lt;li&gt;Tab A, still showing &lt;code&gt;[1]&lt;/code&gt; in its own memory, looks "correct" locally. Refresh Tab A, and Article 1 is gone, because &lt;code&gt;localStorage&lt;/code&gt; only ever had &lt;code&gt;[2]&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nothing in either tab was ever wrong, in isolation. The bug wasn't a logic error — it was two independent sources of truth that agreed by coincidence during testing and diverged under real use, because I always tested with one tab and the user always worked with several.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: listen for the event you're already causing
&lt;/h2&gt;

&lt;p&gt;The browser actually tells you when another tab changes &lt;code&gt;localStorage&lt;/code&gt; — it's just an event most of us never wire up, because single-tab testing never fires it. &lt;code&gt;window&lt;/code&gt; emits a &lt;code&gt;storage&lt;/code&gt; event on every tab &lt;em&gt;except&lt;/em&gt; the one that made the write.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useSavedArticles&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSaved&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;readSaved&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleStorageChange&lt;/span&gt; &lt;span class="o"&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="k"&gt;if &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;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saved-articles&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="nf"&gt;setSaved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;readSaved&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="nb"&gt;window&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="s2"&gt;storage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleStorageChange&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;storage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleStorageChange&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;save&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&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="nf"&gt;setSaved&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saved-articles&lt;/span&gt;&lt;span class="dl"&gt;"&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;next&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;next&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;save&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readSaved&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;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saved-articles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;raw&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&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;Now when Tab A writes, Tab B's &lt;code&gt;storage&lt;/code&gt; listener fires and re-reads from &lt;code&gt;localStorage&lt;/code&gt;, replacing its stale in-memory copy instead of computing a new one from state it never updated. The write in step 3 above now happens against Tab B's &lt;em&gt;current&lt;/em&gt; data, not the snapshot it took when it first mounted.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;BroadcastChannel&lt;/code&gt; is the newer, cleaner alternative if you want structured messages instead of parsing whatever landed in a storage key — but for a simple "resync on external change" case, the &lt;code&gt;storage&lt;/code&gt; event needs no extra API and works in every browser that already supports &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;useState&lt;/code&gt; scopes to a component tree. A component tree scopes to a tab. &lt;code&gt;localStorage&lt;/code&gt; scopes to an origin. The moment your persistence layer is broader than your state's scope, "keep them in sync" stops being optional — it's a requirement you inherited the second you chose &lt;code&gt;localStorage&lt;/code&gt;, whether or not you noticed choosing it.&lt;/p&gt;

&lt;p&gt;I hadn't noticed. I'd been testing the save button, not the two things it was supposed to keep in agreement.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about things that break while I build &lt;a href="https://www.boolflow.site" rel="noopener noreferrer"&gt;Boolflow&lt;/a&gt; and &lt;a href="https://realfeedapp.com" rel="noopener noreferrer"&gt;RealFeedApp&lt;/a&gt;. If you've solved cross-tab sync differently — BroadcastChannel, a shared worker, something else — I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>debugging</category>
    </item>
    <item>
      <title>How I Structure a FastAPI Service: Layers, DI, and Where Validation Actually Belongs</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Sun, 06 Sep 2026 12:00:13 +0000</pubDate>
      <link>https://dev.to/den0011/how-i-structure-a-fastapi-service-layers-di-and-where-validation-actually-belongs-i8m</link>
      <guid>https://dev.to/den0011/how-i-structure-a-fastapi-service-layers-di-and-where-validation-actually-belongs-i8m</guid>
      <description>&lt;p&gt;Every FastAPI tutorial starts the same way: one file, a few &lt;code&gt;@app.get&lt;/code&gt; decorators, a Pydantic model, done. That's great for a demo. It falls apart the moment you have more than one entity, more than one person touching the code, or a bug report that starts with "it validated fine but broke downstream."&lt;/p&gt;

&lt;p&gt;I've hit that wall enough times (see my last post, &lt;a href="https://dev.to/den0011/validationerror-is-not-a-policy-3jpp"&gt;ValidationError Is Not a Policy&lt;/a&gt;, on why a passing &lt;code&gt;ValidationError&lt;/code&gt; check isn't a policy) that I've settled on a structure I now reach for by default. Nothing exotic — no hexagonal-architecture diagrams, no ports-and-adapters ceremony. Just four layers with one job each, and a DI setup that keeps them from knowing too much about one another.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four layers
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
├── api/          # routers — HTTP in, HTTP out, nothing else
├── services/     # business logic — no HTTP, no SQL
├── repositories/ # persistence — no business logic
├── schemas/      # Pydantic models — shape, not meaning
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule I enforce on myself: &lt;strong&gt;each layer is only allowed to talk to the one directly below it.&lt;/strong&gt; A router never touches a repository. A service never sees a &lt;code&gt;Request&lt;/code&gt; object. If I catch myself importing &lt;code&gt;fastapi&lt;/code&gt; inside &lt;code&gt;services/&lt;/code&gt;, that's a signal the logic is in the wrong place.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;api/&lt;/code&gt; — routers
&lt;/h3&gt;

&lt;p&gt;The router's job is translation: turn an HTTP request into a function call, turn a return value into an HTTP response. That's it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;OrderOut&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_order_service&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&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;order&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;try/except&lt;/code&gt; here beyond translating known service exceptions into HTTP status codes. If a router has an &lt;code&gt;if&lt;/code&gt; statement that isn't about status codes, it's doing the service's job.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;services/&lt;/code&gt; — business logic
&lt;/h3&gt;

&lt;p&gt;This is where the actual rules live: what makes an order valid, what happens when stock runs out, which side effects fire in which order. Services depend on repository &lt;em&gt;interfaces&lt;/em&gt;, not concrete database calls — which is what makes them testable without spinning up Postgres.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;InventoryRepository&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderCreate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InsufficientStockError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&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;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;repositories/&lt;/code&gt; — persistence
&lt;/h3&gt;

&lt;p&gt;Repositories know SQL (or whatever storage you use). They don't know &lt;em&gt;why&lt;/em&gt; a row is being saved, only &lt;em&gt;how&lt;/em&gt;. This is the layer I swap out in tests for an in-memory fake.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;schemas/&lt;/code&gt; — Pydantic models
&lt;/h3&gt;

&lt;p&gt;Here's the distinction that took me longest to internalize: &lt;strong&gt;schemas describe shape, services describe meaning.&lt;/strong&gt; A Pydantic model can tell you a field is a positive integer. It cannot tell you that quantity can't exceed available stock, or that a discount code has expired. Those are business rules, and they belong in the service layer — not stuffed into a &lt;code&gt;@validator&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I made this mistake for a while: piling business logic into Pydantic validators because it felt convenient. It works right up until the rule needs to check something outside the payload — a database lookup, another field's async state, today's date against a promo window. Then the validator either can't do it, or does it badly with hidden side effects. Split it: Pydantic checks &lt;em&gt;is this shape well-formed&lt;/em&gt;, the service checks &lt;em&gt;is this operation allowed&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependency injection: keep it boring
&lt;/h2&gt;

&lt;p&gt;FastAPI's &lt;code&gt;Depends()&lt;/code&gt; is enough. I don't reach for a DI container unless the service genuinely has cross-cutting concerns (multi-tenant config, feature flags per request). Most of the time this is all it takes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_order_repository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_order_service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_order_repository&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;InventoryRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_inventory_repository&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="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payoff shows up in tests, not in the app itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_create_order_raises_when_out_of_stock&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;FakeOrderRepository&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;FakeInventoryRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stock&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SKU1&lt;/span&gt;&lt;span class="sh"&gt;"&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="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;InsufficientStockError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SKU1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;TestClient&lt;/code&gt;, no database, no mocking &lt;code&gt;fastapi.Depends&lt;/code&gt;. Just a plain object with fake dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this breaks down
&lt;/h2&gt;

&lt;p&gt;This structure earns its keep once you have real business rules and more than a couple of endpoints. For a small internal tool or a weekend project, it's overkill — a single &lt;code&gt;main.py&lt;/code&gt; is the right call, and I'd tell anyone insisting on four layers for a CRUD toy to stop. The cost is real: more files, more indirection, one extra hop to trace a request end to end.&lt;/p&gt;

&lt;p&gt;The tell that it's time to split things up isn't line count, it's this: the moment a validator needs to ask "but is this actually allowed &lt;em&gt;right now&lt;/em&gt;" instead of "is this shaped correctly," you've outgrown a single file, whether you've noticed it yet or not.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about things that break while I build &lt;a href="https://www.boolflow.site" rel="noopener noreferrer"&gt;Boolflow&lt;/a&gt; and &lt;a href="https://realfeedapp.com" rel="noopener noreferrer"&gt;RealFeedApp&lt;/a&gt;. If you've got a different way of drawing these lines, I'd like to hear where it diverges.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>I Found a 100GB "Bug" Hiding in My NVIDIA Driver</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Fri, 04 Sep 2026 08:42:39 +0000</pubDate>
      <link>https://dev.to/den0011/i-found-a-100gb-bug-hiding-in-my-nvidia-driver-mnc</link>
      <guid>https://dev.to/den0011/i-found-a-100gb-bug-hiding-in-my-nvidia-driver-mnc</guid>
      <description>&lt;p&gt;I was clearing out disk space last week when I noticed one folder eating almost 100GB of my SSD. Not a game. Not a video project. Not Docker images. A cache folder I'd never opened in my life:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\Users\&amp;lt;Username&amp;gt;\AppData\Local\NVIDIA\DXCache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ninety-something gigabytes of &lt;code&gt;.nvph&lt;/code&gt; files with names that mean nothing, sitting quietly on my drive for who knows how long.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually in there
&lt;/h2&gt;

&lt;p&gt;When a game runs, the GPU driver compiles shaders — small programs that tell your graphics card how to render lighting, materials, effects, and so on. Compiling them on the fly causes stutter, so drivers cache the compiled result on disk. Next time you launch the same game, it loads the precompiled shader instead of recompiling it, and you get a smoother experience.&lt;/p&gt;

&lt;p&gt;That part, on its own, isn't the problem. The problem is what happens after — and also &lt;em&gt;why there's so much more of it to cache than there used to be&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this got so much bigger with modern engines
&lt;/h2&gt;

&lt;p&gt;If it feels like shader compilation stutter became a bigger deal in the last few years, that's not just perception. Unreal Engine 4 titles had this problem too, but it was quieter, for a few concrete reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DX11 vs DX12/Vulkan.&lt;/strong&gt; Most UE4 games shipped on DX11, where the driver handles shader compilation itself and hides a lot of the cost. UE5 pushes much harder toward DX12 and Vulkan, and on those APIs, compiling a &lt;strong&gt;Pipeline State Object (PSO)&lt;/strong&gt; is the engine's job, not the driver's. Epic has said outright that runtime hitches from PSO creation are inherent to those APIs in a way they simply aren't on DX11.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Way more PSO combinations.&lt;/strong&gt; Nanite and Lumen introduce dynamic lighting and materials that generate far more unique shader/pipeline-state combinations than UE4's more static rendering paths. More unique PSOs means more first-time compilations happening live, mid-gameplay, which is exactly what shows up as a stutter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The old fix was manual and incomplete.&lt;/strong&gt; UE4 relied on developers manually recording and bundling PSOs into the build ahead of time. It worked, but it was tedious for large projects and still left gaps — Epic's own words were that it "could be burdensome for large projects, and still leave gaps in the cache leading to hitches."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Epic is actively patching this, version by version.&lt;/strong&gt; UE5.1 introduced Automated PSO Gathering to replace the manual process, and UE5.2 added the ability to skip drawing an object if its PSO isn't ready yet instead of stalling the frame. That's a tacit admission this was a real architectural problem, not a one-off bug.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So it's less "this didn't exist in UE4" and more "UE4 could mostly hide it behind DX11, and UE5's push into DX12/Vulkan plus Nanite/Lumen's sheer number of shader permutations made it impossible to hide anymore." Which also means: modern games are compiling and caching &lt;em&gt;more&lt;/em&gt; shader variants than ever, feeding the exact kind of unmanaged, ever-growing cache this whole post is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody tells you
&lt;/h2&gt;

&lt;p&gt;Once a shader is compiled and cached, nothing ever removes it — not when you finish playing the game, not when you uninstall it, not when a driver update makes an old cache entry irrelevant. &lt;code&gt;DXCache&lt;/code&gt; just grows. Forever. One direction only.&lt;/p&gt;

&lt;p&gt;And by default, the NVIDIA Control Panel setting that controls this — &lt;strong&gt;Manage 3D Settings → Shader Cache Size&lt;/strong&gt; — is set to &lt;strong&gt;"Driver Default."&lt;/strong&gt; The full dropdown looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Driver Default&lt;/li&gt;
&lt;li&gt;Disabled&lt;/li&gt;
&lt;li&gt;128 MB&lt;/li&gt;
&lt;li&gt;256 MB&lt;/li&gt;
&lt;li&gt;512 MB&lt;/li&gt;
&lt;li&gt;1 GB&lt;/li&gt;
&lt;li&gt;5 GB&lt;/li&gt;
&lt;li&gt;10 GB&lt;/li&gt;
&lt;li&gt;100 GB&lt;/li&gt;
&lt;li&gt;Unlimited&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the part that surprised me: NVIDIA's own documentation on what "Driver Default" actually resolves to is close to nonexistent. The only official description I could find comes from the driver 495/496.13 release notes, which introduced this setting with a single sentence: &lt;em&gt;"Added Shader Cache Size control to set the maximum amount of disk space to use for storing shader compiles."&lt;/em&gt; That's it. No page explaining what number "Driver Default" maps to, no doc on the eviction policy once a limit is hit, nothing on how it interacts with per-game overrides. It's telling that even developers on NVIDIA's own developer forum have publicly asked whether any detailed documentation of the caching policy exists at all — without getting a concrete answer. So "Driver Default" isn't a documented default in the "this is 2GB, here's why" sense — it's a black box that, in practice, behaves like there's effectively no ceiling, which tracks with mine sitting at nearly 100GB without me ever touching the setting.&lt;/p&gt;

&lt;p&gt;So the math is simple: every game you've ever installed, played for an hour, and uninstalled left a tip behind in &lt;code&gt;DXCache&lt;/code&gt;, and it's still there. I'd been PC gaming for years without ever touching that setting, which is exactly how a folder quietly gets to 100GB without a single warning, notification, or disk-cleanup prompt from Windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worse than it sounds
&lt;/h2&gt;

&lt;p&gt;A few things make this specifically sneaky, rather than just "oh well, cache happens":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It survives uninstalls.&lt;/strong&gt; Deleting a game does not touch its shader cache. The game is gone; the cache is forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows Disk Cleanup doesn't know about it.&lt;/strong&gt; It's not temp files in the usual sense — it's driver-managed and lives outside the paths Windows normally offers to clean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The default has no limit.&lt;/strong&gt; Most caching systems assume &lt;em&gt;someone&lt;/em&gt; eventually caps them. This one ships wide open by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's invisible until you go looking.&lt;/strong&gt; There's no indicator anywhere in the NVIDIA app or Windows that tells you this folder exists, let alone how large it's gotten.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not a bug in the "crash" sense. It's a design decision — unlimited-by-default plus no lifecycle management — that quietly behaves like a slow memory leak, except on your disk, and over years instead of hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DX12 caveat, because I got this wrong at first
&lt;/h2&gt;

&lt;p&gt;Worth being upfront about a wrinkle here: the Shader Cache Size setting in the NVIDIA Control Panel primarily governs the &lt;strong&gt;DX11 and older&lt;/strong&gt; cache path. A lot of modern DX12 and Vulkan titles manage their own shader caching independently, sometimes in different folders, sometimes with their own size limits, sometimes with none. So &lt;code&gt;DXCache&lt;/code&gt; specifically tends to fill up from DX11 titles, but it's not the whole picture — if you're chasing every byte, check your Vulkan cache and any per-game shader cache folders too (many are under &lt;code&gt;%LOCALAPPDATA%&lt;/code&gt; as well, just organized differently per engine).&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did about it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Checked the actual size first.&lt;/strong&gt; Right-click → Properties on the folder, or in PowerShell:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Users\&amp;lt;Username&amp;gt;\AppData\Local\NVIDIA\DXCache"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Measure-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Property&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Sum&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deleted the contents&lt;/strong&gt;, not the folder itself — NVIDIA will rebuild it as needed. The cost of clearing it is a few seconds of shader recompilation stutter the next time you launch each game, which is a fair trade for 100GB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set an actual limit&lt;/strong&gt; in NVIDIA Control Panel → Manage 3D Settings → Global Settings → Shader Cache Size. I put mine at 10GB. Plenty for active games, capped for everything I've since uninstalled.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Worth knowing: once the cache hits that limit, the driver doesn't just stop adding new entries — it starts evicting old ones to make room. So if you play a lot of heavy titles at once and set the cap too low, you can end up with cache getting wiped out and rebuilt constantly, which brings back the exact stutter this whole system exists to prevent. 10GB was comfortable for my library; if you've got a dozen big games installed at once, you may want more headroom.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Made a note to check it again in six months&lt;/strong&gt;, because "set it once and forget it" is exactly the mindset that got me to 100GB in the first place.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The bigger pattern
&lt;/h2&gt;

&lt;p&gt;This isn't really an NVIDIA-specific story once you zoom out. It's the same shape as a dozen other quiet disk hogs: Docker's dangling image layers, npm's global cache, browser service-worker caches, pip's wheel cache. Anything designed to cache the &lt;em&gt;output&lt;/em&gt; of expensive work will keep that output around forever unless something explicitly ties its lifetime to the thing that created it — and "unlimited by default" is a decision, not an oversight, that someone made and shipped.&lt;/p&gt;

&lt;p&gt;Caches without owners don't fail loudly. They just sit there, growing, until you go looking for missing disk space and find out where it's been hiding the whole time.&lt;/p&gt;

&lt;p&gt;If you're on NVIDIA and haven't checked, it might be worth a look:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\Users\&amp;lt;Username&amp;gt;\AppData\Local\NVIDIA\DXCache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might be surprised what's in there.&lt;/p&gt;

</description>
      <category>nvidia</category>
      <category>gaming</category>
      <category>gamedev</category>
      <category>hardware</category>
    </item>
    <item>
      <title>Three Questions I Ask Myself Before Adding a Dependency</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Thu, 03 Sep 2026 20:02:46 +0000</pubDate>
      <link>https://dev.to/den0011/three-questions-i-ask-myself-before-adding-a-dependency-1fjj</link>
      <guid>https://dev.to/den0011/three-questions-i-ask-myself-before-adding-a-dependency-1fjj</guid>
      <description>&lt;p&gt;Every time I'm about to type &lt;code&gt;npm install&lt;/code&gt; or &lt;code&gt;pip install&lt;/code&gt;, I pause for a second. Not because I'm against libraries — I use them every day. It's because I once got burned on a project where &lt;code&gt;package.json&lt;/code&gt; grew to 340 lines, and nobody could explain why half of those packages were there.&lt;/p&gt;

&lt;p&gt;Since then, I ask myself three questions. Nothing philosophical — they take less than a minute, but they save hours of untangling version conflicts and weird bugs six months down the road.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question 1: Can I really not write this myself in 20 lines?
&lt;/h2&gt;

&lt;p&gt;This is the most common source of self-deception. A library looks convenient because it "solves the whole problem," but often I only need 5% of what it does.&lt;/p&gt;

&lt;p&gt;The classic example is &lt;code&gt;left-pad&lt;/code&gt; (yes, the one that broke half of the npm ecosystem in 2016). Padding a string on the left is a one-liner. It doesn't need to be a package with dependencies, tests, and versioning — it's literally:&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;leftPad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&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="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm not saying all utility packages are evil. But if a function fits in 15-20 lines and doesn't need to handle edge cases I'll never actually encounter, I write it myself. The upside: I know exactly how it works when it breaks at 2 a.m. a year from now.&lt;/p&gt;

&lt;p&gt;The check is simple: I open the package source. If it's 200 lines of code and 15 dependencies for functionality I'm using 10% of — that's a red flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question 2: Who maintains this, and what happens if that person disappears tomorrow?
&lt;/h2&gt;

&lt;p&gt;This isn't a question about code quality today — it's about risk a year or two from now. I look at a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When was the last commit.&lt;/strong&gt; Not "how long since the last release" — a package can be stable and just not need updates. What I care about is whether the maintainer responds to issues and PRs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How many maintainers.&lt;/strong&gt; A single person maintaining something in their spare time isn't a dealbreaker, but it's a risk — especially if the package isn't a small utility but something a critical part of my project depends on (auth, money handling, data parsing).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are there active forks.&lt;/strong&gt; If the original project has gone quiet but a fork is thriving, the community may have already voted with its feet, and that's where I should be looking instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's no universal threshold like "three months without a commit is bad." Sometimes a package just does one thing well and there's simply nothing to fix. The real question is: if it breaks, or a vulnerability is found, who fixes it, and how fast?&lt;/p&gt;

&lt;h2&gt;
  
  
  Question 3: What happens to my project if I remove this dependency in a year?
&lt;/h2&gt;

&lt;p&gt;This question is about architectural coupling, not the library itself. I try to picture the scenario in advance: the library goes unmaintained, or I just want to swap it for something else. How deeply has it embedded itself in my code?&lt;/p&gt;

&lt;p&gt;If the dependency is used in one module behind a clean interface, replacing it is a one-day job. If its calls are scattered across the whole project, and its specific abstractions (custom state types, an unusual API) have become part of my own design, I'm stuck with it forever — even if I stop liking it.&lt;/p&gt;

&lt;p&gt;The practical takeaway: I try to wrap third-party libraries behind my own thin abstraction layer, especially for non-critical things like logging, an HTTP client, or date formatting. Not for the sake of "clean architecture" as an abstract virtue, but because it literally lowers the future cost of replacement from "rewrite half the project" to "rewrite one file."&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually looks like in practice
&lt;/h2&gt;

&lt;p&gt;I'm not anti-dependency. I use React, FastAPI, pytest without a second thought, because the answer to all three questions is obvious: writing something comparable myself is unrealistic, maintenance is strong, and the coupling is so fundamental that replacement isn't even a real question.&lt;/p&gt;

&lt;p&gt;But every time I'm about to add a package for something small — date formatting, a debounce function, deep object comparison — I spend a minute on these three questions. Sometimes the answer is "yes, install it." Sometimes I close the terminal and write 10 lines myself.&lt;/p&gt;

&lt;p&gt;Fewer dependencies isn't the goal by itself. But every one I add is a future responsibility I'm taking on without thinking, unless I ask myself these questions first.&lt;/p&gt;

</description>
      <category>python</category>
      <category>productivity</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The Memory Limit That Didn't Kill Anything</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Wed, 02 Sep 2026 17:37:13 +0000</pubDate>
      <link>https://dev.to/den0011/the-memory-limit-that-didnt-kill-anything-38ef</link>
      <guid>https://dev.to/den0011/the-memory-limit-that-didnt-kill-anything-38ef</guid>
      <description>&lt;p&gt;The process that runs my language model has a habit of growing. Not dramatically — it starts at a reasonable size and drifts upward over a shift, and the cause is somewhere in code I don't own. I spent a day trying to configure the leak away, failed, and did the sensible thing instead: stopped trying to fix it and put a ceiling on it, so the damage would be confined to one service instead of the whole box.&lt;/p&gt;

&lt;p&gt;I gave it a soft ceiling. &lt;code&gt;MemoryHigh&lt;/code&gt; in the unit file, a bit under what I'd measured the process to need.&lt;/p&gt;

&lt;p&gt;That is how I turned a leak into an outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything was green
&lt;/h2&gt;

&lt;p&gt;The symptom, when it arrived, was that nothing was wrong. The service was &lt;code&gt;active (running)&lt;/code&gt;. Its PID was current, its log had no errors, and &lt;code&gt;top&lt;/code&gt; showed a process of unremarkable size. Every dashboard I had said the system was up, and it was up. It was just doing a batch of work — the kind that normally takes minutes — for most of a day.&lt;/p&gt;

&lt;p&gt;The thing you have to understand about a soft limit is what the kernel does when you cross it. It doesn't kill the process. It doesn't return an allocation failure. It applies back pressure: it reclaims pages, pushes what it can to swap, and throttles the process's allocations until it fits back under the line. From inside the process, memory still works. Every allocation succeeds. Nothing raises. The program is simply slower, by a factor that has no upper bound, because its working set now lives on a disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A hard limit produces an event. A soft limit produces a symptom.&lt;/strong&gt; &lt;code&gt;MemoryMax&lt;/code&gt; kills the process, systemd restarts it, and you lose one unit of work loudly. &lt;code&gt;MemoryHigh&lt;/code&gt; keeps it alive and makes it useless, and there is no line in any log that says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the truth was
&lt;/h2&gt;

&lt;p&gt;Not in &lt;code&gt;top&lt;/code&gt;. Resident size looked fine — that was the point, the kernel was keeping it fine.&lt;/p&gt;

&lt;p&gt;The truth lives in the cgroup's own counters, which nobody looks at because nothing points you there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/system.slice/&amp;lt;service&amp;gt;/memory.events
&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/system.slice/&amp;lt;service&amp;gt;/memory.swap.current
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;memory.events&lt;/code&gt; has a &lt;code&gt;high&lt;/code&gt; counter — the number of times the process was throttled at the soft ceiling — and an &lt;code&gt;oom_kill&lt;/code&gt; counter. Mine read a throttle count in the millions against zero kills. That pair is the entire diagnosis in two numbers: nothing died, everything was strangled. And &lt;code&gt;memory.swap.current&lt;/code&gt; said that essentially all the swap on the machine belonged to this one service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When a process is slow for no reason, check whether something is holding it under water on purpose.&lt;/strong&gt; A limit you set yourself is the easiest cause to overlook, because you remember configuring it as a safety measure and safety measures aren't suspects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement that caused it
&lt;/h2&gt;

&lt;p&gt;The ceiling wasn't arbitrary. I measured the process's working size and set the limit above it.&lt;/p&gt;

&lt;p&gt;I measured it at the wrong moment. I took the number shortly after the model loaded, which is the calmest instant in that process's life: weights in memory, nothing computed yet, none of the caches and scratch buffers that only exist once real work is running. The number I recorded was a startup value wearing the label "working set". The real figure under load was meaningfully higher, and my ceiling landed underneath it.&lt;/p&gt;

&lt;p&gt;So the process spent its life a few percent over a line it could never get back under, being throttled continuously, on every batch, forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A limit derived from a measurement inherits every flaw in how you measured.&lt;/strong&gt; If the measurement was taken when the system was idle, the limit is a limit on idleness. Sample under load, or don't sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  Slowness is not a local property
&lt;/h2&gt;

&lt;p&gt;Here's the part that turned a slow service into a dead website.&lt;/p&gt;

&lt;p&gt;The worker reads its queue from Postgres and then, in the same transaction, goes off to the model. That was already sloppy, and for a batch that takes minutes it was survivable sloppiness. With the batch now taking most of a day, the worker sat in &lt;code&gt;idle in transaction&lt;/code&gt; for most of a day, holding a perfectly ordinary read lock on one table.&lt;/p&gt;

&lt;p&gt;Then a deploy landed. Startup ran a migration, the migration wanted &lt;code&gt;ALTER TABLE&lt;/code&gt;, and the &lt;code&gt;ALTER&lt;/code&gt; queued behind the worker's read lock. Fine so far — a migration waiting is not an outage.&lt;/p&gt;

&lt;p&gt;Except for the Postgres behaviour that everyone learns exactly once: &lt;strong&gt;a &lt;em&gt;waiting&lt;/em&gt; &lt;code&gt;AccessExclusiveLock&lt;/code&gt; blocks everything that queues up behind it.&lt;/strong&gt; The migration wasn't just waiting, it was a wall. Every subsequent query against that table joined a line behind a lock that would not be granted until a language model finished a job it was never going to finish on time. The API never reached &lt;code&gt;listen&lt;/code&gt;. Its last log line was &lt;code&gt;Waiting for application startup&lt;/code&gt;, and &lt;code&gt;systemctl&lt;/code&gt; reported it &lt;code&gt;active (running)&lt;/code&gt;, which was true and worthless. nginx returned 502 for everything except static images.&lt;/p&gt;

&lt;p&gt;The whole cascade fits in one query, which is the one thing I'd want anyone to take from this:&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="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait_event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;code&gt;active | Lock | ALTER TABLE …&lt;/code&gt; sitting next to an &lt;code&gt;idle in transaction | SELECT …&lt;/code&gt; is not a clue. It's the answer.&lt;/p&gt;

&lt;p&gt;Both sides are fixed now, because both sides were wrong: DDL runs with a &lt;code&gt;lock_timeout&lt;/code&gt; and logs who was holding, and the worker closes its transaction before it goes anywhere near the model. &lt;strong&gt;Never hold a database transaction across a call to something whose duration you don't control&lt;/strong&gt;, and treat a lock without a timeout as a promise to hang the table rather than a promise to wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  The alarm that had been ringing so long it was quiet
&lt;/h2&gt;

&lt;p&gt;I found all of this by accident, which was its own lesson.&lt;/p&gt;

&lt;p&gt;There was an hourly health check, and it had been finding problems the entire time. It said nothing, because alert state was stored as a single boolean for the whole system, and the rule was to notify on change. Chronically elevated swap had set that flag days earlier and it never came back down — so when new failures appeared underneath it, first the worker falling behind, then the API not answering, the state didn't &lt;em&gt;change&lt;/em&gt;, and not one message was sent. Meanwhile the morning summary printed the numbers with no verdict attached and signed off with "all services running."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An alarm that stays silent because the system was already unhealthy is worse than no alarm&lt;/strong&gt;, because it manufactures the feeling of being watched. State has to be tracked per check, not per system; unresolved problems have to keep repeating rather than being deduplicated into silence; and no report gets to print an all-clear until every check has actually passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take from it
&lt;/h2&gt;

&lt;p&gt;I set the soft limit because a hard one felt violent — killing a process over a few megabytes seemed like an overreaction, and back pressure sounded like the gentler, more grown-up choice.&lt;/p&gt;

&lt;p&gt;It's the opposite. The hard limit fails: the process dies, systemd brings it back, one batch is lost, and the log says exactly what happened. The soft limit degrades: nothing fails, nothing is logged, no counter you routinely look at moves, and the system quietly stops doing its job while continuing to report that it is doing its job. One of those is an event you can build on. The other is a fact your monitoring is structurally unable to notice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer the failure you can see to the degradation you can't.&lt;/strong&gt; It's the same rule I keep arriving at from other directions — &lt;a href="https://dev.to/den0011/validationerror-is-not-a-policy-3jpp"&gt;an absent value beats a wrong one&lt;/a&gt; — because the common thread isn't memory or validation. It's that a system which fails loudly is one you can operate, and a system which merely gets worse is one you find out about from your users.&lt;/p&gt;

&lt;p&gt;The limit is gone now. In its place: a hard cap, and swap turned off entirely for that service. If it grows past what the machine can give it, it dies, and I hear about it.&lt;/p&gt;

&lt;p&gt;Both of those are better than what I had, which was a process that was alive the entire time.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>llm</category>
      <category>debugging</category>
      <category>ai</category>
    </item>
    <item>
      <title>ValidationError Is Not a Policy</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Mon, 31 Aug 2026 18:01:21 +0000</pubDate>
      <link>https://dev.to/den0011/validationerror-is-not-a-policy-3jpp</link>
      <guid>https://dev.to/den0011/validationerror-is-not-a-policy-3jpp</guid>
      <description>&lt;p&gt;Every article about data validation ends in the same place. &lt;a href="https://dev.to/den0011/json-and-data-validation-in-python-from-file-reading-to-strict-contracts-27o7"&gt;Mine does too&lt;/a&gt;: define the schema, validate at the boundary, and when the payload doesn't match, raise. That's an honest end to the topic and a dishonest end to the job.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;raise ValidationError&lt;/code&gt; is punctuation. It states that this record is wrong. It says nothing about what the system owes the record next — and that part isn't a matter of taste. It's decided by one question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who is supposed to fix this, and can they?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The tutorial always assumes the same answer: a client sent bad JSON, you return 400, and the client fixes it. That's one boundary out of several, and it's the only one where the fixer is a person who will read your error message.&lt;/p&gt;

&lt;p&gt;I run a science-and-tech digest that has three boundaries inside one process, with three different answers. The validation code is nearly identical in all three. What happens on the line after it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data from strangers: nobody is going to fix it
&lt;/h2&gt;

&lt;p&gt;Feeds are the classic hostile-but-not-malicious input. Publishers ship entries with no link, entries whose title is a chunk of HTML, entries with no date, and occasionally a document that isn't well-formed at all. None of these people are waiting for my error message.&lt;/p&gt;

&lt;p&gt;The first decision isn't whether to reject. It's what to reject.&lt;/p&gt;

&lt;p&gt;The naive version validates the feed: one malformed entry and the whole poll fails, so a publisher's typo silences a source for as long as the typo lives. What I do instead is validate per entry — an entry with no link or no title is skipped, the rest of the batch is kept, and nothing anywhere logs an incident, because this isn't one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reject the smallest thing you can name.&lt;/strong&gt; The unit of rejection is a design decision and it's usually more consequential than the rule that triggered it. Get the unit wrong and a correct validator becomes an outage with good intentions.&lt;/p&gt;

&lt;p&gt;The second decision is subtler, because sometimes I don't reject at all. Entries without a date get stamped with the time I saw them. That is, plainly, a fabricated value — the kind of thing every validation article tells you not to do. I do it anyway, because I can say exactly what it costs: the item is at most slightly too fresh, freshness decays over hours, and it leaves the ranking window on its own. What it buys is that the article exists at all, and a dropped article never comes back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A default is a lie you've decided you can afford.&lt;/strong&gt; Which is fine, as long as you can name the price. If you can't, you haven't chosen a fallback — you've chosen not to think about the case.&lt;/p&gt;

&lt;p&gt;The third decision has nothing to do with validity. Some feeds — WordPress archives especially — hand you their entire history on a normal poll. Every entry perfectly valid, every field present, and accepting all of them would bury everything published this morning under a decade of old posts. So there's a ceiling on how many entries I take from one feed per poll.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validity says nothing about quantity, and quantity is its own failure mode.&lt;/strong&gt; A schema will happily certify ten thousand records that shouldn't have arrived together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data from a generator you invited: there's nobody to return the error to
&lt;/h2&gt;

&lt;p&gt;This is the boundary the tutorials don't cover, and it's the one that made me write this. When a model returns a translation my checks reject, the 400 has no recipient. There's no client to notify, no caller to fix its request, no human on the other end of the connection.&lt;/p&gt;

&lt;p&gt;Detecting the bad value is its own story, and I've &lt;a href="https://dev.to/den0011/the-schema-was-valid-the-translation-was-in-chinese-3cfa"&gt;told it separately&lt;/a&gt;: a schema-valid response can hold the right number of fields with the wrong language inside them, or a correct headline with a foreign clause welded onto its tail — which a share-based check waves through every time, because by proportion the correct text wins comfortably. The short version is that structured output guarantees shape and nothing about meaning, so the semantic validator is yours to write and it's about as much code as the schema.&lt;/p&gt;

&lt;p&gt;Here I care about the line after that check. Not how the bad value is caught, but what the rejection &lt;em&gt;becomes&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If a rejected translation is simply dropped, that's not validation, that's deletion. The next pass through the pipeline takes fresh work off the top; it never revisits what quietly disappeared. Rejecting a value and moving on feels rigorous and is indistinguishable, downstream, from never having tried.&lt;/p&gt;

&lt;p&gt;What makes it real is that the rejection leaves a shape behind. The slot stays empty rather than being filled with the bad value, and empty is exactly what the regeneration path looks for. The record's own state is the queue. Nothing needs to be enqueued, retried, or remembered elsewhere, because absence is already the signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When there's nobody to return the error to, the rejection has to become a work item — or it's a deletion with better manners.&lt;/strong&gt; And the corollary that decides your data model: never store a wrong value where an absent one would fit, because absence is a state the system can act on and wrongness looks exactly like success to every query you'll ever write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data from yourself: stop
&lt;/h2&gt;

&lt;p&gt;The third boundary is configuration, and its policy is the opposite of the first two. Skipping is wrong. Substituting is wrong. Continuing is wrong.&lt;/p&gt;

&lt;p&gt;The database URL in this project used to have a sensible-looking default pointing at a local file. With an unconfigured environment the application started perfectly, connected to an empty database, served an empty feed, and reported nothing unusual, because nothing unusual had happened by its own account. The symptom presented as "the feed is quiet today" rather than "the database was never configured", and those two look identical from the outside for as long as you're willing to keep looking.&lt;/p&gt;

&lt;p&gt;The default is gone. The process now refuses to start and says which variable is missing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dangerous default is the one that works.&lt;/strong&gt; A fallback that crashes on first use is a typo with a stack trace. A fallback that lets the process come up healthy is a phantom system: fully operational, structurally sound, connected to nothing. Config isn't the place for resilience — there is no degraded mode worth having when the thing that's misconfigured is what the service is &lt;em&gt;for&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The line after the validator
&lt;/h2&gt;

&lt;p&gt;Same library, same schema shape, three boundaries, three policies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;from strangers — skip the smallest failing unit, substitute only where you can price the lie, and cap the volume separately from the validity;&lt;/li&gt;
&lt;li&gt;from a generator — turn the rejection into a state the pipeline reads as unfinished work;&lt;/li&gt;
&lt;li&gt;from yourself — refuse to start.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is derivable from the schema, because the schema doesn't know who's on the other side of the boundary. It knows what the data should look like. Whether a wrong record is somebody else's problem, your system's own backlog, or a reason to stop the process is a question about ownership, and it's yours to answer for every boundary you draw.&lt;/p&gt;

&lt;p&gt;Validation tells you the data is wrong. It never tells you what you owe it.&lt;/p&gt;




&lt;p&gt;This is the third of three on the same seam, and they read in any order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://dev.to/den0011/json-and-data-validation-in-python-from-file-reading-to-strict-contracts-27o7"&gt;JSON and Data Validation in Python&lt;/a&gt; — the contract: &lt;code&gt;json&lt;/code&gt;, &lt;code&gt;jsonschema&lt;/code&gt;, Pydantic, and where to put the boundary.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/den0011/the-schema-was-valid-the-translation-was-in-chinese-3cfa"&gt;The Schema Was Valid. The Translation Was in Chinese.&lt;/a&gt; — what a schema doesn't buy you when the producer is a language model.&lt;/li&gt;
&lt;li&gt;This one — what the system does on the line after the rejection.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All three come out of the same &lt;a href="https://realfeedapp.com" rel="noopener noreferrer"&gt;science-and-tech digest&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>json</category>
      <category>validation</category>
      <category>architecture</category>
    </item>
    <item>
      <title>JSON and Data Validation in Python: From File Reading to Strict Contracts</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Mon, 31 Aug 2026 14:34:52 +0000</pubDate>
      <link>https://dev.to/den0011/json-and-data-validation-in-python-from-file-reading-to-strict-contracts-27o7</link>
      <guid>https://dev.to/den0011/json-and-data-validation-in-python-from-file-reading-to-strict-contracts-27o7</guid>
      <description>&lt;p&gt;&lt;strong&gt;JSON (JavaScript Object Notation)&lt;/strong&gt; is a text-based data interchange format that has become a de facto standard in modern web development. Thanks to its simplicity, readability, and language independence, JSON is used everywhere: for exchanging data between clients and servers through APIs, storing configuration files such as &lt;code&gt;settings.json&lt;/code&gt;, and recording application events in logs.&lt;/p&gt;

&lt;p&gt;In the Python ecosystem, JSON support is available directly in the standard library, while data validation can be handled with specialized libraries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working with JSON in Python
&lt;/h2&gt;

&lt;p&gt;You don't need to install any third-party packages to work with JSON. Python's standard library includes the &lt;code&gt;json&lt;/code&gt; module, which can convert JSON strings and files into native Python structures such as dictionaries and lists, and convert them back to JSON.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading JSON from a file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the JSON data is already available as a string, for example as a response received from an external API, use &lt;code&gt;loads()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;json_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;John&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: 42}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# Output: 42
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The distinction is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;json.load()&lt;/code&gt; reads JSON from a file-like object.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;json.loads()&lt;/code&gt; parses JSON from a string.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Writing JSON back to a file
&lt;/h3&gt;

&lt;p&gt;It is important to remember that the &lt;code&gt;json&lt;/code&gt; module works with objects in memory. If you modify the data, you must explicitly write it back to a file if you want to persist the changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="c1"&gt;# Add a new field
&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;company&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tech Company&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&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;indent=4&lt;/code&gt; parameter formats the JSON with indentation, making the file easier for humans to read.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ensure_ascii=False&lt;/code&gt; keeps non-ASCII characters readable instead of converting them into Unicode escape sequences.&lt;/p&gt;

&lt;p&gt;For example, without it, text such as:&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;0410&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;043d&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;0434&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;0440&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;0435&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;0439"&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;may be produced instead of:&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;h2&gt;
  
  
  Why Do We Need Data Validation?
&lt;/h2&gt;

&lt;p&gt;Imagine that your application receives JSON from a mobile client.&lt;/p&gt;

&lt;p&gt;A developer working on the mobile application accidentally renames the &lt;code&gt;user_id&lt;/code&gt; field to &lt;code&gt;userid&lt;/code&gt;. Your backend may then fail when it tries to access the missing key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&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 results in a &lt;code&gt;KeyError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An even more dangerous situation occurs when the structure is correct but the data type is not. For example, the server expects an integer:&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;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&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;but receives:&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;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"twenty-five"&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;An operation expecting a number may then fail with a &lt;code&gt;TypeError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is why applications often use &lt;strong&gt;schemas&lt;/strong&gt; and &lt;strong&gt;data validation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A schema describes the expected structure of the data: which fields are allowed, which fields are required, what types their values must have, and what additional constraints apply.&lt;/p&gt;

&lt;p&gt;In other words, a schema acts as a &lt;strong&gt;contract between different parts of a system&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strict Validation with &lt;code&gt;jsonschema&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;You can write validation manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, as the data structure becomes more complicated, manually maintaining dozens or hundreds of such checks quickly becomes difficult.&lt;/p&gt;

&lt;p&gt;A more standardized approach is the &lt;a href="https://python-jsonschema.readthedocs.io/" rel="noopener noreferrer"&gt;&lt;code&gt;jsonschema&lt;/code&gt;&lt;/a&gt; library.&lt;/p&gt;

&lt;p&gt;Install it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;jsonschema
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's define a strict user contract.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;jsonschema&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Define the contract
&lt;/span&gt;&lt;span class="n"&gt;user_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minLength&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;integer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minimum&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;boolean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;additionalProperties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Data received from an external source
&lt;/span&gt;&lt;span class="n"&gt;incoming_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;john_smith&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;john@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Validate the data
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;incoming_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data is valid. It can be stored in the database.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data validation error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# The API could return HTTP 400 Bad Request here.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;additionalProperties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It means that fields not explicitly defined in the schema are rejected.&lt;/p&gt;

&lt;p&gt;For example, this object would fail validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;incoming_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;john_smith&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;because &lt;code&gt;role&lt;/code&gt; is not part of the contract.&lt;/p&gt;

&lt;p&gt;This can be extremely useful for APIs where silently accepting unexpected fields could lead to bugs or security problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  More Complex Validation Rules
&lt;/h2&gt;

&lt;p&gt;JSON Schema supports much more than simple type checking.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;enum&lt;/code&gt; restricts a value to a predefined set:&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enum"&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="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"editor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"viewer"&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;Only one of these three values will be accepted.&lt;/p&gt;

&lt;p&gt;Regular expressions can be used with &lt;code&gt;pattern&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^[A-Za-z]+$"&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 example allows only Latin letters.&lt;/p&gt;

&lt;p&gt;JSON Schema can also describe arrays, nested objects, numeric limits, string lengths, conditional requirements, and relationships between fields.&lt;/p&gt;

&lt;p&gt;As a result, even fairly complex API contracts can be described declaratively instead of implementing every rule manually in Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  An Alternative Approach: Pydantic and Type Hints
&lt;/h2&gt;

&lt;p&gt;There is another popular approach in modern Python applications: using &lt;strong&gt;Pydantic&lt;/strong&gt; models.&lt;/p&gt;

&lt;p&gt;Instead of describing the structure as a dictionary containing a JSON Schema, you describe it using Python classes and type annotations.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmailStr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EmailStr&lt;/span&gt;
    &lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;


&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;incoming_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach provides several useful features.&lt;/p&gt;

&lt;p&gt;Pydantic can parse incoming data and, where appropriate, convert compatible values to the declared Python types.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;incoming_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;john_smith&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;25&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;john@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on the field and Pydantic configuration, the string &lt;code&gt;"25"&lt;/code&gt; can be parsed as the integer &lt;code&gt;25&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The resulting object is no longer just an arbitrary dictionary. It is a &lt;code&gt;User&lt;/code&gt; instance with a defined structure and validated fields.&lt;/p&gt;

&lt;p&gt;The default value also means that &lt;code&gt;is_active&lt;/code&gt; does not have to be supplied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EmailStr&lt;/span&gt;
    &lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the field is omitted, Pydantic uses &lt;code&gt;True&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pydantic in API Development
&lt;/h2&gt;

&lt;p&gt;Pydantic is widely used in modern Python web applications and is a core part of frameworks such as &lt;strong&gt;FastAPI&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One of its major advantages is that the same model can serve multiple purposes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validate incoming data;&lt;/li&gt;
&lt;li&gt;provide typed Python objects;&lt;/li&gt;
&lt;li&gt;serialize data back to JSON;&lt;/li&gt;
&lt;li&gt;describe API schemas;&lt;/li&gt;
&lt;li&gt;generate API documentation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes Pydantic particularly convenient when building typed REST APIs.&lt;/p&gt;

&lt;p&gt;Instead of passing unvalidated dictionaries throughout the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you can work with a well-defined model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference becomes increasingly important as the application grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical API Validation Workflow
&lt;/h2&gt;

&lt;p&gt;In a typical backend application, validation should happen at the system boundary — before untrusted external data reaches the business logic.&lt;/p&gt;

&lt;p&gt;The workflow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
   │
   ▼
JSON request
   │
   ▼
Validation
   │
   ├── Invalid ──► HTTP 400
   │
   ▼
Validated data
   │
   ▼
Business logic
   │
   ▼
Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The basic process is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive the JSON request body.&lt;/li&gt;
&lt;li&gt;Parse the JSON.&lt;/li&gt;
&lt;li&gt;Validate it against the expected schema or model.&lt;/li&gt;
&lt;li&gt;If validation fails, collect the validation errors.&lt;/li&gt;
&lt;li&gt;Return a structured HTTP error response, typically with a &lt;code&gt;4xx&lt;/code&gt; status code.&lt;/li&gt;
&lt;li&gt;Only after successful validation pass the data to the business logic.&lt;/li&gt;
&lt;li&gt;Store or process the validated data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This creates a clear boundary between &lt;strong&gt;untrusted external input&lt;/strong&gt; and &lt;strong&gt;internal application state&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Strict Contracts Matter
&lt;/h2&gt;

&lt;p&gt;Without validation, every function that receives external data has to protect itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then another function adds its own checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And another one checks something else.&lt;/p&gt;

&lt;p&gt;Over time, this leads to duplicated validation logic scattered throughout the codebase.&lt;/p&gt;

&lt;p&gt;A schema or model moves these checks to a single, well-defined boundary.&lt;/p&gt;

&lt;p&gt;After validation succeeds, the rest of the application can operate under a much stronger assumption:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If the data reached the business logic, it already satisfies the contract.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That does not eliminate the need for error handling, but it dramatically reduces the number of defensive checks required throughout the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  JSON Is More Than Just a File Format
&lt;/h2&gt;

&lt;p&gt;JSON itself is simple. The difficult part is usually not parsing JSON — it is ensuring that the data represented by JSON actually matches what the application expects.&lt;/p&gt;

&lt;p&gt;For small scripts, the standard &lt;code&gt;json&lt;/code&gt; module is usually enough.&lt;/p&gt;

&lt;p&gt;For APIs and larger applications, schema validation becomes increasingly important. Depending on the architecture, &lt;code&gt;jsonschema&lt;/code&gt;, Pydantic, or another validation system can turn loosely structured JSON into a well-defined data contract.&lt;/p&gt;

&lt;p&gt;The result is not just cleaner code. It is a system where the boundaries between components are explicit, errors are detected earlier, and changes to data structures are much easier to control.&lt;/p&gt;

</description>
      <category>python</category>
      <category>json</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The Schema Was Valid. The Translation Was in Chinese</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Mon, 31 Aug 2026 14:16:55 +0000</pubDate>
      <link>https://dev.to/den0011/the-schema-was-valid-the-translation-was-in-chinese-3cfa</link>
      <guid>https://dev.to/den0011/the-schema-was-valid-the-translation-was-in-chinese-3cfa</guid>
      <description>&lt;p&gt;Constraining a model's output with a JSON schema feels like the moment the LLM stops being a text generator and becomes a function. You declare the fields, the server enforces the grammar, and what comes back parses every time. After months of regex-scraping JSON out of prose, it's a genuine relief.&lt;/p&gt;

&lt;p&gt;Then you ship it, and you find out what a schema actually promises. It promises a shape. Everything you care about is content, and content is on you.&lt;/p&gt;

&lt;p&gt;Here's the list of things that came back schema-valid and wrong, in the order I found them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The array was the right shape and the wrong length
&lt;/h2&gt;

&lt;p&gt;The model processes several items per call and returns one object per item. The schema says: an array of objects, each with these required fields.&lt;/p&gt;

&lt;p&gt;Sometimes it returned one object for three inputs. Perfectly valid — an array of one is an array. The schema constrains what an element looks like; it has no idea how many elements you were expecting, because it never saw your input.&lt;/p&gt;

&lt;p&gt;The fix is boring and unavoidable: &lt;strong&gt;reconcile the response against the request by id.&lt;/strong&gt; Send ids in, require ids back, diff the sets, and re-ask individually for whatever went missing. What made this worth catching rather than shrugging at is what silence would have cost — a dropped item doesn't come back around on the next pass, because the next pass selects a fresh top slice of new work. "Probably fine, it'll get picked up later" is only true if something actually picks it up later.&lt;/p&gt;

&lt;p&gt;The same reconciliation gives you a free failure mode for the whole batch: if the entire call times out, retry the items one at a time instead of losing all of them to one pathological input.&lt;/p&gt;

&lt;h2&gt;
  
  
  The field was a string, and it was the wrong language
&lt;/h2&gt;

&lt;p&gt;Each item comes back with a translations object: one key per target language, each holding a title and summary.&lt;/p&gt;

&lt;p&gt;A small model mixes those slots up. Under the key for one language, you get fluent text in a different one — sometimes the source language untranslated, sometimes a language nobody asked for. The schema is entirely satisfied: the required key is present, its value is an object, the fields are strings. All true. All useless.&lt;/p&gt;

&lt;p&gt;So you write the check the schema can't: does this text use the writing system its language key implies? Count characters by script, compare to what's expected for that key, reject the mismatch.&lt;/p&gt;

&lt;p&gt;This works, and it's the easy half.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard half: the wrong language glued onto the right one
&lt;/h2&gt;

&lt;p&gt;The check above compares shares — the language's own script should dominate. That catches wholesale substitution and misses the failure that actually reached my users.&lt;/p&gt;

&lt;p&gt;The model sometimes finishes a translation correctly and then keeps going, appending a second translation into the same field. What ships looks like a correct headline with a foreign clause welded to its tail. By share, the correct text wins comfortably — the tail is a fraction of the string. The dominance check passed it every time, and a batch of headlines went out with a foreign fragment stapled on the end.&lt;/p&gt;

&lt;p&gt;The fix isn't a better ratio. It's noticing that ratios are the wrong tool here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A threshold that looks reasonable will catch the common case and wave through the dangerous one.&lt;/strong&gt; Any share-based test assumes the two things being weighed are commensurable. Correct output and corrupt output aren't. Two characters of a script that has no business appearing is not a small problem in proportion to the string — it's the whole failure, at any size.&lt;/p&gt;

&lt;p&gt;So the rule became presence, not proportion: scripts that cannot legitimately appear in this language are rejected on sight, at a count of one.&lt;/p&gt;

&lt;p&gt;The nuance that makes it survivable is knowing where &lt;em&gt;not&lt;/em&gt; to apply zero tolerance. Latin characters stay legal everywhere, because product names, acronyms and companies are legitimate in any language, and banning them would reject far more good translations than it saves. &lt;strong&gt;Zero tolerance is only affordable where you can name the reason no legitimate case exists.&lt;/strong&gt; Otherwise you're just building a stricter bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The field was populated, and the contents were filler
&lt;/h2&gt;

&lt;p&gt;The schema language tempts you to push semantics into it. My keyword list looked like a great candidate: require at least three entries, and the model has to produce three.&lt;/p&gt;

&lt;p&gt;It does. It pads. Ask a summarizer for a minimum of three keywords per science article and the shortfall gets filled with &lt;code&gt;study&lt;/code&gt;, &lt;code&gt;research&lt;/code&gt;, &lt;code&gt;science&lt;/code&gt; — perfectly valid strings, perfectly useless index terms, and worse than an empty list because now they're indistinguishable from real ones.&lt;/p&gt;

&lt;p&gt;I dropped the minimum. The schema's job is to guarantee the field exists and holds strings; how many there should be is a question about meaning, which belongs to the prompt. An upper bound stayed, and only because it matches a truncation the code performs anyway — no point paying to generate items you're about to throw away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A schema can force a field to exist. It cannot force it to be worth having.&lt;/strong&gt; Every constraint you add gets satisfied by the cheapest available route, and for a minimum count the cheapest route is filler.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do with a value you've rejected
&lt;/h2&gt;

&lt;p&gt;Having caught a bad translation, the instinct is to keep it — something is better than nothing, surely.&lt;/p&gt;

&lt;p&gt;It isn't. I drop it and leave the slot empty, which gets you two things. The reader sees an honest fallback to the original language instead of confident nonsense in theirs. And an empty slot is a work item: a later pass sees a missing translation and regenerates it, whereas a filled slot containing garbage looks done forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer an absent field to a wrong one.&lt;/strong&gt; Absence is a state your system can act on. Wrongness looks exactly like success to every piece of code downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things about the plumbing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Not every server accepts your schema.&lt;/strong&gt; Older runtimes reject the schema object outright with a 400. Retrying without it is right, but retry &lt;em&gt;and remember&lt;/em&gt; — flip a flag for the process, so you're not paying two round trips for every call from then on. Degrade to prompt-only and log it loudly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parse defensively even with a grammar.&lt;/strong&gt; Whether the wrapper is an array, a sequence of bare objects, or a single object varies with the model and whether the schema survived. Rather than requiring one canonical shape, scan for top-level &lt;code&gt;{...}&lt;/code&gt; and decode each one incrementally. It's a few lines, it's shape-agnostic, and it turns "the model wrapped it differently today" from an outage into a non-event.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take away from it
&lt;/h2&gt;

&lt;p&gt;Structured output moved a whole class of problems — malformed JSON, missing keys, prose wrapped around the payload — from my code into the runtime, and that's a real win I wouldn't give back.&lt;/p&gt;

&lt;p&gt;What it didn't do is make the output correct. It relocated the boundary. The grammar handles syntax; everything about meaning is still yours, and the failures that reach production are all on the meaning side, because syntax errors are loud and semantic ones are polite. Wrong language in the right slot, a foreign clause on the tail, three words of filler in a keyword list — every one of those was valid against the schema and would have been caught by five minutes of looking at what actually shipped.&lt;/p&gt;

&lt;p&gt;So budget for the validator you write yourself, and expect it to be about as much code as the schema. Mine runs on a science-and-tech digest &lt;a href="https://realfeedapp.com" rel="noopener noreferrer"&gt;I maintain&lt;/a&gt;, where the model translates into several languages at once and every one of them is a slot it can put the wrong thing into. The schema stopped me from ever parsing a broken response. It never once stopped me from publishing a wrong one.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Everything worked when I looked at it. That was the bug.</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Mon, 31 Aug 2026 13:03:57 +0000</pubDate>
      <link>https://dev.to/den0011/everything-worked-when-i-looked-at-it-that-was-the-bug-562h</link>
      <guid>https://dev.to/den0011/everything-worked-when-i-looked-at-it-that-was-the-bug-562h</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/den0011/i-know-what-to-build-next-my-own-architecture-disagrees-about-the-order-2hi5"&gt;the last post&lt;/a&gt;&lt;br&gt;
I opened the source of &lt;a href="https://boolflow.site" rel="noopener noreferrer"&gt;Boolflow&lt;/a&gt;, my browser-based&lt;br&gt;
digital logic simulator, and found that the roadmap I had confidently published&lt;br&gt;
was in the wrong order. I ended it planning to build an explicit tick counter.&lt;/p&gt;

&lt;p&gt;I did not build the tick counter. I opened the deployment instead, and spent the&lt;br&gt;
day finding four things that had been broken for months while looking completely&lt;br&gt;
fine.&lt;/p&gt;

&lt;p&gt;That is the thread running through all of them. Not one of these bugs made&lt;br&gt;
anything fail in front of me. Every single one was hidden by the normal path&lt;br&gt;
working exactly as intended.&lt;/p&gt;
&lt;h2&gt;
  
  
  The prerenderer I wrote in June went live in August
&lt;/h2&gt;

&lt;p&gt;Boolflow is a Vite SPA with a prerender step: &lt;code&gt;prerender.mjs&lt;/code&gt; walks a route&lt;br&gt;
table, renders each page with &lt;code&gt;renderToString&lt;/code&gt;, and writes a real HTML file per&lt;br&gt;
URL. I wrote it on 4 June. It has been sitting in the repository ever since,&lt;br&gt;
correct and tested and doing nothing at all.&lt;/p&gt;

&lt;p&gt;Here is what production actually served, on every URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/                          200  4493 bytes
/help                      200  4493 bytes
/ru/help                   200  4493 bytes
/ru/articles/karnaugh-maps 200  4493 bytes
/verilog                   200  4493 bytes
/nonexistent-page-xyz      200  4493 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same 4.5 KB on all of them. Same &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;. Same description. &lt;code&gt;&amp;lt;div&lt;br&gt;
id="root"&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt; — empty. It was the Vite build output &lt;em&gt;before&lt;/em&gt; the prerender&lt;br&gt;
step overwrote it.&lt;/p&gt;

&lt;p&gt;The cause is three lines of &lt;code&gt;package.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="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="s2"&gt;"vite build"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"prerender"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"node prerender.mjs"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"build:full"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"npm run build &amp;amp;&amp;amp; npm run build:server &amp;amp;&amp;amp; npm run prerender"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And one line of my own deployment guide, step 11, "Updating the application":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prerender lived in &lt;code&gt;build:full&lt;/code&gt;. The documented update procedure called&lt;br&gt;
&lt;code&gt;build&lt;/code&gt;. Not sometimes — &lt;em&gt;structurally&lt;/em&gt;, every time, for three months. The&lt;br&gt;
deployment instructions I wrote could not have produced a prerendered site if I&lt;br&gt;
had followed them perfectly.&lt;/p&gt;

&lt;p&gt;And here is why I never noticed: &lt;strong&gt;the site looks perfect in a browser.&lt;/strong&gt; You&lt;br&gt;
request &lt;code&gt;/ru/help&lt;/code&gt;, get 4.5 KB of empty shell, the JavaScript loads, React&lt;br&gt;
hydrates, and the Russian help page appears. Every human visitor, including me,&lt;br&gt;
saw a working site. Only crawlers — and anything else that does not execute&lt;br&gt;
JavaScript — saw the shell.&lt;/p&gt;

&lt;p&gt;The fix is embarrassingly small. &lt;code&gt;build&lt;/code&gt; is now the whole pipeline:&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="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;"npm run build:client &amp;amp;&amp;amp; npm run build:server &amp;amp;&amp;amp; npm run prerender"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"build:client"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vite build"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can no longer deploy half a build, because there is no longer a script that&lt;br&gt;
produces half a build.&lt;/p&gt;
&lt;h2&gt;
  
  
  Meanwhile, every page told Google it was the homepage
&lt;/h2&gt;

&lt;p&gt;The shell has a static &lt;code&gt;&amp;lt;link rel="canonical" href="https://boolflow.site/"&amp;gt;&lt;/code&gt; —&lt;br&gt;
correct for the landing page, meaningless for the other sixty-four URLs it was&lt;br&gt;
being served as.&lt;/p&gt;

&lt;p&gt;So the sitemap advertised 65 URLs, and all 65 responded with the same title, the&lt;br&gt;
same description, and a canonical tag pointing at the homepage. That is not&lt;br&gt;
"pages ranking badly." That is me explicitly instructing Google to collapse the&lt;br&gt;
entire site into one page.&lt;/p&gt;

&lt;p&gt;Three smaller things were wrong in the same layer, and all three had the same&lt;br&gt;
character — a thing that returns 200 and looks fine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/nonexistent-page-xyz&lt;/code&gt; returned &lt;strong&gt;200&lt;/strong&gt;. Every typo was an indexable page.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;www.boolflow.site&lt;/code&gt; returned &lt;strong&gt;200&lt;/strong&gt; with no redirect, so the whole site
existed twice, on two hosts, with canonicals pointing at only one of them.&lt;/li&gt;
&lt;li&gt;The sitemap was hand-maintained next to a script that already knew the exact
route list. It had drifted, of course. It now gets generated from that route
list, so it cannot advertise a URL that has no page.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The URL said Russian. The code asked localStorage.
&lt;/h2&gt;

&lt;p&gt;Boolflow has EN/RU/DE. Articles and help live under &lt;code&gt;/ru/…&lt;/code&gt; and &lt;code&gt;/de/…&lt;/code&gt;. The&lt;br&gt;
language context read the language from exactly one place:&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLang&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;boolflow-lang&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EN&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;p&gt;The URL is not in that expression. So &lt;code&gt;/ru/help&lt;/code&gt; rendered its &lt;em&gt;body&lt;/em&gt; from the&lt;br&gt;
path and its &lt;em&gt;chrome&lt;/em&gt; from localStorage, and the prerendered file proves it — a&lt;br&gt;
Russian &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; under an English header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dist/ru/help  →  header: "How to use Boolflow"
                 h1:     "Справка Boolflow"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worse, the provider also set &lt;code&gt;document.documentElement.lang&lt;/code&gt; from localStorage,&lt;br&gt;
so hydration rewrote the prerendered &lt;code&gt;lang="ru"&lt;/code&gt; back to &lt;code&gt;lang="en"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The URL prefix now wins, falling back to the stored preference only on paths&lt;br&gt;
that have no prefix. One consequence I did not expect: the moment the URL became&lt;br&gt;
authoritative, I found the translations for the landing page and all six tool&lt;br&gt;
pages were &lt;strong&gt;already written&lt;/strong&gt; — full EN/RU/DE copy, sitting in the source,&lt;br&gt;
reachable only as a localStorage state. They had no addresses. Google had never&lt;br&gt;
seen them and could not have.&lt;/p&gt;

&lt;p&gt;Giving them addresses took an afternoon and added fourteen pages of content that&lt;br&gt;
already existed.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bug that survived because it was usually idempotent
&lt;/h2&gt;

&lt;p&gt;This is the one I am actually pleased about, because it validated the last post&lt;br&gt;
in a way I did not intend.&lt;/p&gt;

&lt;p&gt;Stateful elements — flip-flops, counters, latches — keep their state in a&lt;br&gt;
module-level &lt;code&gt;Map&lt;/code&gt;, keyed by node id, mutated in place:&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;st&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nodeStates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;q&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="na"&gt;prevClk&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;q&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;clk&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prevClk&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="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&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="c1"&gt;// rising edge&lt;/span&gt;
&lt;span class="nx"&gt;nodeStates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;prevClk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clk&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;simulate()&lt;/code&gt; runs more than once per logical tick. Boolflow has four layers, and&lt;br&gt;
signals cross between them through named VIA elements, so the layer loop&lt;br&gt;
simulates everything once, discovers the VIA values, then re-simulates the&lt;br&gt;
layers that received them.&lt;/p&gt;

&lt;p&gt;Normally that is harmless. Run it twice with the same inputs and you get the&lt;br&gt;
same answer, because &lt;code&gt;prevClk&lt;/code&gt; has already caught up and there is no edge left&lt;br&gt;
to detect.&lt;/p&gt;

&lt;p&gt;Except a VIA delivers its value &lt;em&gt;on the second pass&lt;/em&gt;. So:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clock goes 0 → 1. Pass one runs. &lt;code&gt;D&lt;/code&gt; is still unresolved, &lt;code&gt;null&lt;/code&gt;. The
flip-flop sees the rising edge and latches &lt;code&gt;0&lt;/code&gt;. &lt;code&gt;prevClk&lt;/code&gt; becomes 1.&lt;/li&gt;
&lt;li&gt;The VIA value arrives. The layer is re-simulated with the real &lt;code&gt;D&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prevClk&lt;/code&gt; is already 1. There is no edge. Nothing happens.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The flip-flop latches a stale value, and only when its data crosses a layer&lt;br&gt;
boundary — which is the entire reason the layer feature exists.&lt;/p&gt;

&lt;p&gt;Here is the test, replaying the layer loop verbatim with the same flip-flop wired&lt;br&gt;
two ways:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;D flip-flop, D held at 1, one rising clock edge. Expected Q = 1.

  same layer, direct wire   -&amp;gt;  Q = 1   ok
  D arrives through a VIA   -&amp;gt;  Q = 0   WRONG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to stop mutating one map. State is now split: &lt;code&gt;committed&lt;/code&gt; is what it&lt;br&gt;
was at the start of the tick, &lt;code&gt;working&lt;/code&gt; is what is being computed during it.&lt;br&gt;
Every pass reads from &lt;code&gt;committed&lt;/code&gt;, so repeated passes &lt;em&gt;recompute the same&lt;br&gt;
transition&lt;/em&gt; with progressively better inputs instead of racing to consume the&lt;br&gt;
edge first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  same layer, direct wire   -&amp;gt;  Q = 1   ok
  D arrives through a VIA   -&amp;gt;  Q = 1   ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the part I like. In the last post I argued that an explicit tick counter had&lt;br&gt;
to come before waveforms and sequential tests, and I argued it on &lt;em&gt;architectural&lt;/em&gt;&lt;br&gt;
grounds — the engine has no notion of time, so features that need time cannot be&lt;br&gt;
built honestly. It was a good argument and it was entirely theoretical.&lt;/p&gt;

&lt;p&gt;It turns out the engine not knowing when a tick begins was not a future problem.&lt;br&gt;
It was corrupting multi-layer clocked circuits in production. The invisible&lt;br&gt;
refactor I was defending on taste was a bug fix with a reproduction case.&lt;/p&gt;
&lt;h2&gt;
  
  
  A captcha that rendered perfectly and could not be read
&lt;/h2&gt;

&lt;p&gt;Admin login has an image captcha. I could not get past it. My first assumption&lt;br&gt;
was that I was typing it wrong.&lt;/p&gt;

&lt;p&gt;The endpoint is healthy: &lt;code&gt;200&lt;/code&gt;, &lt;code&gt;image/png&lt;/code&gt;, 11875 bytes, correct &lt;code&gt;https&lt;/code&gt; URL.&lt;br&gt;
So I downloaded three and looked at them. &lt;code&gt;4U2S2&lt;/code&gt; — the last &lt;code&gt;2&lt;/code&gt; sliced off by&lt;br&gt;
the right edge. &lt;code&gt;W3NPQ&lt;/code&gt; — the &lt;code&gt;Q&lt;/code&gt; running into the border.&lt;/p&gt;

&lt;p&gt;Then the arithmetic. Each character is drawn into a 36×42 box and rotated up to&lt;br&gt;
25° with &lt;code&gt;expand=1&lt;/code&gt;, which makes it &lt;em&gt;bigger&lt;/em&gt; than its box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;36·cos25° + 42·sin25° ≈ 50 px wide
36·sin25° + 42·cos25° ≈ 53 px tall
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The canvas was &lt;code&gt;150 × 50&lt;/code&gt;. A single glyph did not fit vertically. And with a&lt;br&gt;
start of &lt;code&gt;x = 10&lt;/code&gt; and a random advance of 22–26 px, the fifth character began at&lt;br&gt;
98–114 and ended at 148–164 — off the right edge of a 150 px image.&lt;/p&gt;

&lt;p&gt;Two round numbers, chosen once, never checked against the thing they had to&lt;br&gt;
hold. The canvas is now derived from the glyph geometry rather than guessed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;canvas: 169 × 60   glyph: 49 × 52
right edge of last character: 161 ≤ 169
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also dropped the remaining lookalike characters — &lt;code&gt;Q Z 2 S 5 B 8 6 V&lt;/code&gt; on top of&lt;br&gt;
the &lt;code&gt;O 0 I 1 L&lt;/code&gt; that were already excluded — leaving 22 characters and about 5.1&lt;br&gt;
million combinations, which is plenty for something that allows five guesses.&lt;/p&gt;

&lt;p&gt;To check it honestly I generated four captchas without looking at the answers,&lt;br&gt;
read them, and only then compared. Three for three.&lt;/p&gt;
&lt;h2&gt;
  
  
  The tool pages had nothing to say to a crawler
&lt;/h2&gt;

&lt;p&gt;An SEO audit flagged three URLs as thin. I counted the rendered words myself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    9  /check         "All connections are complete."
   13  /truth-table   "No inputs or outputs on this layer."
   22  /verilog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The audit's advice was the usual boilerplate about ensuring sufficient word&lt;br&gt;
count. Word count is not a ranking factor and padding pages is how you get&lt;br&gt;
useless pages, so I ignored the advice — but the diagnosis was correct, and the&lt;br&gt;
reason was structural rather than editorial.&lt;/p&gt;

&lt;p&gt;These pages render the result of processing a circuit. With no circuit there is&lt;br&gt;
nothing to render. And a crawler will &lt;em&gt;never&lt;/em&gt; load a circuit — nor will a human&lt;br&gt;
arriving from a search for "convert logic circuit to verilog", who lands on an&lt;br&gt;
empty box and leaves.&lt;/p&gt;

&lt;p&gt;So each tool now carries an explanatory section that does not depend on editor&lt;br&gt;
state: what the tool does, a real sample of its output, the limitations, a link&lt;br&gt;
to the relevant article. The samples are taken from what the exporters actually&lt;br&gt;
emit, because a fake code sample in a page about generating code would be a&lt;br&gt;
strange thing to ship.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   22 → 235  /verilog        9 → 219  /check
   13 → 220  /truth-table   21 → 259  /cpp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the day actually cost
&lt;/h2&gt;

&lt;p&gt;Honest accounting: I closed one of five roadmap items, and only half of it —&lt;br&gt;
combinational test cases, which turned out to need none of the refactors I said&lt;br&gt;
they needed. Everything else was infrastructure, and none of it was on any plan.&lt;/p&gt;

&lt;p&gt;What changed for users: nothing visible. What changed for everyone who has ever&lt;br&gt;
searched for a logic simulator: eighty-one pages that were previously&lt;br&gt;
unreachable, in three languages, are now crawlable for the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;I keep coming back to the shape of these four.&lt;/p&gt;

&lt;p&gt;The site rendered. The flip-flop latched. The captcha returned a valid PNG. The&lt;br&gt;
tool pages worked when you used them. In every case the thing I would naturally&lt;br&gt;
check was the thing that was working, and the failure lived one step off the path&lt;br&gt;
I ever walked: no JavaScript, a signal arriving late, the fifth character, an&lt;br&gt;
empty editor.&lt;/p&gt;

&lt;p&gt;The uncomfortable version of this lesson is that the more reliable your happy&lt;br&gt;
path, the better it hides everything else. I have no clean method to offer. The&lt;br&gt;
only thing that worked was going and looking at the artifact directly — &lt;code&gt;curl&lt;/code&gt;&lt;br&gt;
the URL instead of opening it, count the bytes, download the image and actually&lt;br&gt;
look at it, replay the loop with the state printed.&lt;/p&gt;

&lt;p&gt;Every one of these was found by looking at the output rather than at the code&lt;br&gt;
that produces it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Boolflow is free and open in the browser at &lt;a href="https://boolflow.site" rel="noopener noreferrer"&gt;boolflow.site&lt;/a&gt;.&lt;br&gt;
The tick counter is next. This time I have a reproduction case for why.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>debugging</category>
    </item>
    <item>
      <title>I know what to build next. My own architecture disagrees about the order</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Wed, 26 Aug 2026 12:07:23 +0000</pubDate>
      <link>https://dev.to/den0011/i-know-what-to-build-next-my-own-architecture-disagrees-about-the-order-2hi5</link>
      <guid>https://dev.to/den0011/i-know-what-to-build-next-my-own-architecture-disagrees-about-the-order-2hi5</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/den0011/i-compared-my-logic-simulator-to-9-others-here-is-where-mine-loses-38p7"&gt;the last post&lt;/a&gt;&lt;br&gt;
I compared &lt;a href="https://boolflow.site" rel="noopener noreferrer"&gt;Boolflow&lt;/a&gt;, my browser-based&lt;br&gt;
digital logic simulator, against nine other tools and came away with three&lt;br&gt;
honest gaps: no sub-circuits, no waveforms, no test cases. I ended that article&lt;br&gt;
with a tidy numbered list and the confidence of a man who had not yet opened the&lt;br&gt;
files.&lt;/p&gt;

&lt;p&gt;Then I opened the files.&lt;/p&gt;

&lt;p&gt;This post is what I found. It is a roadmap, but it is the kind of roadmap you&lt;br&gt;
get &lt;em&gt;after&lt;/em&gt; reading your own code rather than before — which is to say the order&lt;br&gt;
changed, the list got longer, and the reason is a single design decision I made&lt;br&gt;
about eighteen months ago without noticing I was making it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fact underneath everything
&lt;/h2&gt;

&lt;p&gt;Here is the whole simulation entry point. Fifty-six lines, and I am showing you&lt;br&gt;
the load-bearing half:&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;simulate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;seedVals&lt;/span&gt; &lt;span class="o"&gt;=&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;vals&lt;/span&gt; &lt;span class="o"&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;seedVals&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// ... seed INPUT / CLOCK / CONST nodes into vals ...&lt;/span&gt;

  &lt;span class="nf"&gt;simulateComb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;combNodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// pass 1: upstream combinational&lt;/span&gt;
  &lt;span class="nf"&gt;simulateStateful&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;statefulNodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;simulateComb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;combNodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// pass 2: downstream&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;vals&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;Three phases. Combinational, then stateful, then combinational again. It takes a&lt;br&gt;
flat list of nodes and edges and returns a flat object of wire values.&lt;/p&gt;

&lt;p&gt;"Phase" rather than "pass", because each combinational phase is itself a loop —&lt;br&gt;
it relaxes the whole network repeatedly until, in principle, everything settles:&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;simulateComb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;combNodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vals&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;iters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;combNodes&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;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pass&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="nx"&gt;pass&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;iters&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="o"&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;combNodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* evaluate one gate into vals */&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;Look at what is missing. There is no time parameter. There is no delay model, no&lt;br&gt;
event queue, no notion of "before" and "after". &lt;code&gt;simulate()&lt;/code&gt; does not advance a&lt;br&gt;
simulation — it computes what all the wires settle to, right now, given the&lt;br&gt;
current inputs. Call it twice with the same arguments and, for a purely&lt;br&gt;
combinational circuit, you get the same answer twice.&lt;/p&gt;

&lt;p&gt;Look also at what that loop does &lt;em&gt;not&lt;/em&gt; do: it never checks whether anything&lt;br&gt;
changed. It runs its full &lt;code&gt;2N&lt;/code&gt; iterations every single time, and it has no way to&lt;br&gt;
report that the network failed to settle. I will come back to both of those at&lt;br&gt;
the end, because together they turn out to be the cheapest thing on this entire&lt;br&gt;
page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Boolflow does not simulate time. It computes steady states.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is not a bug. For a teaching tool it is arguably the right call: a beginner&lt;br&gt;
toggling an INPUT wants to see the output change, not to reason about&lt;br&gt;
nanosecond-scale propagation. It is also why the whole engine is under 600 lines&lt;br&gt;
and runs instantly on every keystroke.&lt;/p&gt;

&lt;p&gt;But it means time in Boolflow is &lt;em&gt;emergent&lt;/em&gt;. It exists only because something&lt;br&gt;
else happens to call &lt;code&gt;simulate()&lt;/code&gt; again. And what calls &lt;code&gt;simulate()&lt;/code&gt; again is&lt;br&gt;
React:&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;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&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="nf"&gt;setLayers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ls&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CLOCK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;running&lt;/span&gt; &lt;span class="o"&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="nx"&gt;n&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&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="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&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;span class="p"&gt;},&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;freq&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;setInterval&lt;/code&gt; flips the CLOCK node's value, React re-renders, a &lt;code&gt;useMemo&lt;/code&gt;&lt;br&gt;
recomputes the simulation. One clock tick equals one React render.&lt;/p&gt;

&lt;p&gt;Every item on my roadmap collides with that sentence.&lt;/p&gt;
&lt;h2&gt;
  
  
  Sub-circuits: the problem is not drawing, it is identity
&lt;/h2&gt;

&lt;p&gt;The visible half of sub-circuits is easy and boring: save a circuit, show it in&lt;br&gt;
the palette, draw it as a box with pins. A weekend, maybe two.&lt;/p&gt;

&lt;p&gt;The invisible half is this file, and specifically these four lines:&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;// Persistent state for edge-triggered / stateful elements.&lt;/span&gt;
&lt;span class="c1"&gt;// Lives at module level so state survives across simulate() calls.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodeStates&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;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;clearNodeState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;nodeStates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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;Every flip-flop, latch, counter and register in Boolflow keeps its state in one&lt;br&gt;
module-level &lt;code&gt;Map&lt;/code&gt;, keyed by node id. A D flip-flop reads and writes it like&lt;br&gt;
this:&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;st&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nodeStates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;q&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="na"&gt;prevClk&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;q&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;clk&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prevClk&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="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&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="nx"&gt;nodeStates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;prevClk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clk&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;prevClk&lt;/code&gt;. That is how edge detection works: compare this call's clock value&lt;br&gt;
to the previous call's. The flip-flop's entire sense of time is "the last time&lt;br&gt;
somebody ran the simulation." It is a global mutable Map standing in for a&lt;br&gt;
clock, and for a flat circuit it works perfectly well.&lt;/p&gt;

&lt;p&gt;Now instantiate a sub-circuit containing that flip-flop twice.&lt;/p&gt;

&lt;p&gt;Both instances contain a node whose id is, say, &lt;code&gt;n7&lt;/code&gt;. Both read and write&lt;br&gt;
&lt;code&gt;nodeStates.get('n7')&lt;/code&gt;. Your two counters are now the &lt;em&gt;same&lt;/em&gt; counter, wired to&lt;br&gt;
different pins, fighting over one entry in a Map. Put a 4-bit register inside a&lt;br&gt;
block and drop four of them on the canvas, and all four hold identical data&lt;br&gt;
forever.&lt;/p&gt;

&lt;p&gt;So sub-circuits are not a drawing feature. They are a request to make node&lt;br&gt;
identity &lt;strong&gt;hierarchical&lt;/strong&gt;: a node is no longer &lt;code&gt;n7&lt;/code&gt;, it is&lt;br&gt;
&lt;code&gt;inst3/inst1/n7&lt;/code&gt; — a path from the top-level sheet down through every enclosing&lt;br&gt;
instance.&lt;/p&gt;

&lt;p&gt;There are two ways out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recursive evaluation.&lt;/strong&gt; Teach &lt;code&gt;simulate()&lt;/code&gt; to descend into a sub-circuit,&lt;br&gt;
evaluate it with its own scoped state, and return its outputs. Conceptually&lt;br&gt;
clean. It also means threading an instance path through every function that&lt;br&gt;
touches &lt;code&gt;nodeStates&lt;/code&gt;, and it makes the value map — currently a flat&lt;br&gt;
&lt;code&gt;{"n7:out-0": 1}&lt;/code&gt; — into something nested or path-keyed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flattening.&lt;/strong&gt; When a sub-circuit is placed, inline a copy of its nodes and&lt;br&gt;
edges into the parent, rewriting every id to &lt;code&gt;instanceId/originalId&lt;/code&gt;. The&lt;br&gt;
simulator never learns that hierarchy exists; it keeps receiving one flat list,&lt;br&gt;
just a longer one. All the work moves into the editor layer and one id-rewriting&lt;br&gt;
function.&lt;/p&gt;

&lt;p&gt;I am going with flattening, for three reasons. It leaves the simulation engine&lt;br&gt;
almost untouched, which matters when the engine is the part I trust most. It&lt;br&gt;
gives correct per-instance state for free, because rewritten ids are unique by&lt;br&gt;
construction. And it is what &lt;a href="https://github.com/hneemann/Digital" rel="noopener noreferrer"&gt;Digital&lt;/a&gt; does&lt;br&gt;
— its docs note that embedded circuits are included as often as the circuit is&lt;br&gt;
used — which is reassuring, because Digital is the most rigorously built tool in&lt;br&gt;
this category.&lt;/p&gt;

&lt;p&gt;The cost is honest and worth stating: flattening a block used fifty times means&lt;br&gt;
fifty copies in memory, and a deep hierarchy multiplies. A tool built for CPU&lt;br&gt;
scale would not accept that. Boolflow is built for someone learning what a&lt;br&gt;
register is, and at that scale I will take the trade.&lt;/p&gt;
&lt;h2&gt;
  
  
  Waveforms: I have to make time real first
&lt;/h2&gt;

&lt;p&gt;A timing diagram plots signal values against time. I have signal values. I do&lt;br&gt;
not have time.&lt;/p&gt;

&lt;p&gt;What I have is a &lt;code&gt;setInterval&lt;/code&gt; that mutates a React node and hopes. To draw a&lt;br&gt;
waveform I need three things that do not exist yet:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;An explicit tick counter&lt;/strong&gt; — a monotonically increasing integer that says
which simulation step we are on, owned by the simulator rather than inferred
from React's render cycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A history buffer&lt;/strong&gt; — a ring buffer recording the value map at each tick,
for the last N ticks. This part is genuinely easy; &lt;code&gt;simulate()&lt;/code&gt; already
returns exactly the object that needs recording.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A step control&lt;/strong&gt; — the ability to advance one tick deliberately, so a
learner can walk a shift register through its states instead of watching it
blur past at 4 Hz.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Item 3 is the one I actually care about. Watching a counter run is mildly&lt;br&gt;
interesting; stepping it one clock edge at a time while reading the flip-flop&lt;br&gt;
outputs is where the concept lands. Logicly has had step-by-step propagation&lt;br&gt;
debugging for years and it is the single feature of theirs I have been quietly&lt;br&gt;
jealous of.&lt;/p&gt;

&lt;p&gt;While reading this code I also found a bug I had never noticed:&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;freq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allClocks&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;freq&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every CLOCK element on the canvas ticks on one shared interval, running at&lt;br&gt;
whichever clock happens to be first in the array. Give two clocks different&lt;br&gt;
frequencies and Boolflow will cheerfully ignore you. Nobody has reported it,&lt;br&gt;
which I suspect means nobody has tried — but the fix belongs in exactly this&lt;br&gt;
refactor, because a real tick counter makes per-clock division trivial instead&lt;br&gt;
of awkward.&lt;/p&gt;

&lt;p&gt;There is one thing a waveform in Boolflow will &lt;em&gt;not&lt;/em&gt; show, and I would rather say&lt;br&gt;
it here than have someone find out the hard way: glitches. With no propagation&lt;br&gt;
delay model, a hazard that a real circuit would exhibit as a brief spurious pulse&lt;br&gt;
simply does not exist in my engine. The diagram will be idealised. For teaching&lt;br&gt;
what a shift register does, idealised is fine and possibly better. For teaching&lt;br&gt;
why you need a synchroniser, it is a lie, and the metastability article I wrote&lt;br&gt;
will have to keep saying so in words.&lt;/p&gt;
&lt;h2&gt;
  
  
  Test cases: the cheap one, for a good reason
&lt;/h2&gt;

&lt;p&gt;Of the three features this is the least work, and it is worth understanding why,&lt;br&gt;
because the reason is not that testing is simple.&lt;/p&gt;

&lt;p&gt;Boolflow already has a truth table generator. To build a truth table for a&lt;br&gt;
combinational circuit you enumerate every input combination, force the INPUT&lt;br&gt;
nodes to each combination in turn, run the simulation, and read the OUTPUT&lt;br&gt;
nodes. That is: &lt;em&gt;set inputs, evaluate, collect outputs&lt;/em&gt;, in a loop, headless,&lt;br&gt;
without touching the canvas.&lt;/p&gt;

&lt;p&gt;That is also exactly what a test runner does. The only thing a test case adds is&lt;br&gt;
a column of expected values and a comparison. The machinery is already written&lt;br&gt;
and already shipping.&lt;/p&gt;

&lt;p&gt;For combinational circuits I could have this working in a couple of evenings.&lt;br&gt;
For sequential circuits — the ones where testing actually earns its keep — a&lt;br&gt;
test case is a &lt;em&gt;sequence&lt;/em&gt; of steps with expected outputs after each one, which&lt;br&gt;
means it needs the tick counter from the waveform work. So it is cheap, but only&lt;br&gt;
after something else is done.&lt;/p&gt;

&lt;p&gt;Digital's model is the one to copy: declare inputs and expected outputs in a&lt;br&gt;
table, run, get pass or fail. It is also the missing half of "exercises with&lt;br&gt;
checkable answers", which I promised readers two articles ago and have not&lt;br&gt;
delivered.&lt;/p&gt;
&lt;h2&gt;
  
  
  So the roadmap has five items, not three
&lt;/h2&gt;

&lt;p&gt;Reading the code turned a list of three features into a dependency graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make time explicit ──────┬──→ waveforms + step mode
                         └──→ sequential test cases

hierarchical node identity ───→ sub-circuits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which gives an actual order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Explicit tick counter and history buffer.&lt;/strong&gt; Invisible to users. Unblocks two
of the three features and fixes the shared-clock-frequency bug on the way
past.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step mode and the waveform panel.&lt;/strong&gt; The first visible payoff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hierarchical node ids by flattening.&lt;/strong&gt; Also invisible, also a prerequisite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sub-circuits.&lt;/strong&gt; The largest gap against every other tool in the comparison.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test cases&lt;/strong&gt;, combinational first, sequential once step mode lands.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two of the five ship with nothing to show for them. That is the part I did not&lt;br&gt;
expect when I wrote the confident numbered list last month, and it is the honest&lt;br&gt;
shape of the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four more things the comparison suggested, and one trap
&lt;/h2&gt;

&lt;p&gt;The five above are commitments. These are candidates — things I noticed while&lt;br&gt;
looking at other tools, none of which I have promised anyone. I am listing them&lt;br&gt;
because the cheapest one is genuinely embarrassing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tell the user the circuit did not settle.&lt;/strong&gt; Remember that relaxation loop with&lt;br&gt;
no convergence check. Add a comparison of &lt;code&gt;vals&lt;/code&gt; before and after each iteration&lt;br&gt;
and you get two things from about ten lines: an early &lt;code&gt;break&lt;/code&gt; when the network is&lt;br&gt;
stable, which stops a hundred-node circuit from grinding through two hundred&lt;br&gt;
pointless iterations on every render — and the ability to say "this circuit is&lt;br&gt;
oscillating" when the loop runs out without settling.&lt;/p&gt;

&lt;p&gt;Right now a ring of three inverters just exhausts its iteration budget and&lt;br&gt;
returns whatever state it happened to stop on. Boolflow reports a confident,&lt;br&gt;
wrong answer with no warning at all. Oscillation detection is one of the marquee&lt;br&gt;
reasons people move from Logisim to Digital; in my engine it costs ten lines and&lt;br&gt;
I simply had not thought to look.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A link to a circuit.&lt;/strong&gt; Falstad, CircuitVerse and DigiSim all let you send&lt;br&gt;
someone a URL. Boolflow lets you download a JSON file and email it, like it is&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The awkward part is that the backend for this already exists — the
endpoints to store a scheme and fetch it by id have been written and working for
months, and nothing in the UI ever calls them. This is not a feature, it is a
missing button.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;An &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; embed.&lt;/strong&gt; This follows almost free from the link, and of&lt;br&gt;
everything on this page it is the item with the best odds of mattering. In the v4&lt;br&gt;
post I wrote that a browser tool has almost nothing for a search engine to index&lt;br&gt;
and that this might decide whether anyone ever finds the project. An embed is the&lt;br&gt;
answer to exactly that: every live circuit on somebody else's course page or blog&lt;br&gt;
post is both a working demo and a way in. It is the only item here that&lt;br&gt;
compounds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A truth table that turns back into a circuit.&lt;/strong&gt; Boolflow already goes from&lt;br&gt;
circuit to truth table. Digital goes the other way too — you type the table you&lt;br&gt;
want and it synthesises and minimises the logic. That is a self-contained module&lt;br&gt;
that emits nodes and edges into the existing structures and touches the engine&lt;br&gt;
not at all, and it would give the Karnaugh map article something to link to&lt;br&gt;
instead of leaving the reader with nowhere to click. In the desktop tier Digital&lt;br&gt;
has this. In the browser tier, essentially nobody does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the trap: buses.&lt;/strong&gt; Multi-bit wires sit innocently in a feature list next to&lt;br&gt;
those four, and CircuitVerse and LogicCircuit both have them. But today a wire&lt;br&gt;
value is one bit — &lt;code&gt;vals&lt;/code&gt; is a flat map of &lt;code&gt;"n7:out-0" → 1&lt;/code&gt;. Making a port carry&lt;br&gt;
four bits rewrites every gate in the combinational evaluator, all three&lt;br&gt;
exporters, the wire renderer and the port definitions. It is the single most&lt;br&gt;
invasive item anyone could reasonably ask me for, and it looks like the smallest.&lt;br&gt;
I am not touching it before sub-circuits exist.&lt;/p&gt;

&lt;p&gt;The pattern I did not expect: three of those four are cheap precisely because&lt;br&gt;
they do not involve the simulator. The expensive work is all in the engine, and&lt;br&gt;
the engine is the part I was proudest of.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am still not building
&lt;/h2&gt;

&lt;p&gt;Unchanged from the comparison post: no accounts, no groups, no assignments, no&lt;br&gt;
grading, no teacher dashboards. CircuitVerse has done all of that properly for&lt;br&gt;
years, and there is no version of this where I do it better alone. If you need a&lt;br&gt;
classroom platform, use CircuitVerse — I mean that as a recommendation, not a&lt;br&gt;
concession.&lt;/p&gt;

&lt;p&gt;Boolflow's job is to be the thing you open in a tab at 2am with no account and no&lt;br&gt;
install, build something, and understand it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions I would like answered
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flattening versus recursive evaluation&lt;/strong&gt; — has anyone here built hierarchy
into a simulator and regretted flattening? The memory cost is the obvious
objection; I want to hear the non-obvious one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idealised waveforms&lt;/strong&gt; — if you teach digital logic, is a timing diagram with
no propagation delay useful, or is it actively misleading? I can fake a
uniform unit delay per gate, but faking it convincingly may be worse than
admitting the model has no delay at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step mode granularity&lt;/strong&gt; — one clock edge per step, or one gate evaluation per
step? Digital does single-gate stepping specifically to debug oscillation.
Clock-edge stepping is simpler and probably what a learner wants. I keep
changing my mind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is two invisible refactors out of five a reasonable thing to publish?&lt;/strong&gt; I
went back and forth on whether a roadmap post should only contain things users
can see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embeds versus sub-circuits&lt;/strong&gt; — the disciplined answer is to finish the five
before touching the candidate list. The other answer is that an embed takes a
fraction of the effort and is the only item that brings new people in. If you
have run a small project past this fork, I would like to know which way you
went.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Boolflow is at &lt;strong&gt;&lt;a href="https://boolflow.site" rel="noopener noreferrer"&gt;boolflow.site&lt;/a&gt;&lt;/strong&gt; — free, no account,&lt;br&gt;
nothing to install. &lt;/p&gt;

&lt;p&gt;Tell me which of these you would actually use, and I will believe you over my own&lt;br&gt;
dependency graph.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>architecture</category>
    </item>
    <item>
      <title>I compared my logic simulator to 9 others. Here is where mine loses</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Wed, 26 Aug 2026 07:04:57 +0000</pubDate>
      <link>https://dev.to/den0011/i-compared-my-logic-simulator-to-9-others-here-is-where-mine-loses-38p7</link>
      <guid>https://dev.to/den0011/i-compared-my-logic-simulator-to-9-others-here-is-where-mine-loses-38p7</guid>
      <description>&lt;p&gt;I have written four posts about Boolflow now — &lt;a href="https://dev.to/den0011/i-built-a-free-browser-based-digital-logic-simulator-for-learning-boolean-algebra-57m7"&gt;the first release&lt;/a&gt;, &lt;a href="https://dev.to/den0011/boolflow-v2-a-major-update-to-my-free-browser-based-digital-logic-simulator-56ac"&gt;v2&lt;/a&gt;, &lt;a href="https://dev.to/den0011/boolflow-v3-improving-the-first-time-experience-of-my-browser-based-digital-logic-simulator-39pa"&gt;v3&lt;/a&gt; and &lt;a href="https://dev.to/den0011/boolflow-v4-i-added-a-17-article-learning-section-to-my-browser-based-logic-simulator-fg1"&gt;v4&lt;/a&gt;. Every one of them was about what I added.&lt;/p&gt;

&lt;p&gt;None of them answered the question I actually get asked, which is some polite variant of: &lt;em&gt;Logisim exists. Why does this?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For a long time I did not have a real answer, because I had never done the work. So I did it. I spent a couple of weeks with nine other digital logic simulators — installing them, building the same handful of circuits in each, reading their docs and their source where it was available — and then I put my own tool in the same table and looked at the result.&lt;/p&gt;

&lt;p&gt;The result is not flattering in the places you would expect, and it is better than I thought in one place I did not expect. Both halves are below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The field
&lt;/h2&gt;

&lt;p&gt;There is no single "logic simulator market". There are three clusters that barely compete with each other, plus one commercial outlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Desktop, free, open source.&lt;/strong&gt; &lt;a href="https://github.com/logisim-evolution/logisim-evolution" rel="noopener noreferrer"&gt;Logisim Evolution&lt;/a&gt; is the default answer in most university courses — GPL-3.0, Java, 7.5k stars, and still actively developed (v4.1.0 landed in February 2026). &lt;a href="https://github.com/hneemann/Digital" rel="noopener noreferrer"&gt;Digital&lt;/a&gt; by Helmut Neemann is the connoisseur's pick: GPL-3.0, 5.9k stars, and in several areas — test cases, circuit analysis, hardware export — genuinely ahead of Logisim. &lt;a href="https://www.logiccircuit.org/" rel="noopener noreferrer"&gt;LogicCircuit&lt;/a&gt; is a free open-source Windows/.NET tool with unrestricted circuit hierarchy, multi-bit buses, a built-in oscilloscope and — since 2024 — HDL export to Verilog and Nand2Tetris. &lt;a href="https://www.digitalelectronicsdeeds.com/" rel="noopener noreferrer"&gt;Deeds&lt;/a&gt; comes out of the University of Genoa and is the most &lt;em&gt;curricular&lt;/em&gt; of the four — combinational and sequential design, an FSM editor, and a microcomputer emulator with a custom 8-bit CPU, all wrapped in course material. Windows only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser, free.&lt;/strong&gt; &lt;a href="https://circuitverse.org/" rel="noopener noreferrer"&gt;CircuitVerse&lt;/a&gt; is the serious one: MIT-licensed, subcircuits, timing diagrams, propagation delay control, Verilog modules, testbenches, and a full classroom layer with groups, assignments, deadlines, grading and grade export. &lt;a href="https://www.falstad.com/circuit/" rel="noopener noreferrer"&gt;Falstad's CircuitJS1&lt;/a&gt; is GPL-2.0 and is really an analog simulator with digital parts — the animated current flow is unmatched for intuition, but it is not organised around gate-to-CPU teaching. &lt;a href="https://simulator.io/" rel="noopener noreferrer"&gt;simulator.io&lt;/a&gt; is clean and account-free to try, has real-time collaboration, and has a 2015 copyright line in its footer that tells you most of what you need to know about its development pace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commercial.&lt;/strong&gt; &lt;a href="https://logic.ly/" rel="noopener noreferrer"&gt;Logicly&lt;/a&gt; is Windows and macOS, aimed squarely at teachers, with custom ICs and step-by-step propagation debugging. It costs $59 for a student licence, $599 for a classroom and $1,299 for a campus — one-time, not subscription.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And then there is &lt;a href="https://digisim.io/" rel="noopener noreferrer"&gt;DigiSim&lt;/a&gt;&lt;/strong&gt;, which I found late and which is the closest thing to a direct competitor Boolflow has. Browser-based, real-time, four labs (digital, relay, electricity, analog), 61 components, narrated lessons in seven languages, freemium — the free tier gives you 16 of 61 components, Pro is $6/month, classroom is $4/seat/year. It is doing the same thing I am doing — putting the lessons &lt;em&gt;inside&lt;/em&gt; the simulator — with a business model and more languages. Finding it was the least fun afternoon of this whole exercise, and also the most useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Boolflow&lt;/strong&gt;, for the table below: browser-only, free, no account, 74 element types, 20 worked examples, exports Verilog / VHDL / C++, generates truth tables, four design layers linked by VIA nodes, and ships 17 theory articles in English, Russian and German.&lt;/p&gt;

&lt;h2&gt;
  
  
  The table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Runs in&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Sub-circuits&lt;/th&gt;
&lt;th&gt;HDL export&lt;/th&gt;
&lt;th&gt;Waveforms&lt;/th&gt;
&lt;th&gt;Classroom&lt;/th&gt;
&lt;th&gt;Account&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Logisim Evolution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Desktop (Java)&lt;/td&gt;
&lt;td&gt;Free, GPL-3.0&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;VHDL + Verilog&lt;/td&gt;
&lt;td&gt;Chronogram&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;Digital&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Desktop (Java)&lt;/td&gt;
&lt;td&gt;Free, GPL-3.0&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;VHDL + Verilog + JEDEC&lt;/td&gt;
&lt;td&gt;Measurement graphs&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;LogicCircuit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Desktop (Windows)&lt;/td&gt;
&lt;td&gt;Free, open source&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Verilog + Nand2Tetris&lt;/td&gt;
&lt;td&gt;Oscilloscope&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;Deeds&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Desktop (Windows)&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;VHDL&lt;/td&gt;
&lt;td&gt;Timing diagrams&lt;/td&gt;
&lt;td&gt;Course material&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CircuitVerse&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser&lt;/td&gt;
&lt;td&gt;Free, MIT&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Verilog&lt;/td&gt;
&lt;td&gt;Timing diagram&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;To save online&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Falstad&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser&lt;/td&gt;
&lt;td&gt;Free, GPL-2.0&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Scope&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;simulator.io&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;To save&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Logicly&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Desktop + web demo&lt;/td&gt;
&lt;td&gt;$59 / $599 / $1,299&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Step debug&lt;/td&gt;
&lt;td&gt;Teacher-oriented&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DigiSim&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser&lt;/td&gt;
&lt;td&gt;Free tier / $6 mo&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Paid tier&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Boolflow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Verilog + VHDL + &lt;strong&gt;C++&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The bold entries in my own row are the ones that matter. Three of them are gaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Boolflow loses
&lt;/h2&gt;

&lt;p&gt;I want to be specific here rather than doing the humble-brag thing where the weakness is secretly a strength.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. No sub-circuits. This is the big one.
&lt;/h3&gt;

&lt;p&gt;Every serious tool in that table lets you build a circuit, save it, and drop it into another circuit as a single block. Logisim, Digital, LogicCircuit, Deeds, CircuitVerse, Logicly, DigiSim — all of them. It is the feature that takes you from "I built a full adder" to "I built a 4-bit ALU out of four full adders" to "I built a CPU".&lt;/p&gt;

&lt;p&gt;Boolflow does not have it. What Boolflow has is &lt;em&gt;layers&lt;/em&gt; — up to four independent sheets connected by VIA nodes — and I have caught myself describing them as if they solve the same problem. They do not. Layers are a way to stop one canvas becoming unreadable. Sub-circuits are a way to build abstractions. One is a viewing convenience, the other is the thing computer architecture is &lt;em&gt;made of&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Writing the v4 article series taught digital logic. Writing this comparison taught me that my tool tops out exactly where the interesting part of the subject begins.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. No timing diagrams.
&lt;/h3&gt;

&lt;p&gt;Logisim has a chronogram. Digital has measurement graphs. LogicCircuit has an oscilloscope. CircuitVerse has timing diagrams with configurable propagation delay. Falstad has a scope.&lt;/p&gt;

&lt;p&gt;Boolflow shows you the current state of every wire, and nothing else. For combinational logic that is genuinely fine — the truth table generator covers it. For anything clocked, it is a real handicap. You cannot debug a shift register by staring at its present value. I wrote an entire article about metastability for a tool that cannot draw you a single waveform, which in retrospect is funny.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. No test framework, no classroom layer.
&lt;/h3&gt;

&lt;p&gt;Digital lets you write test cases and run them against a circuit. CircuitVerse has testbenches where an author declares input vectors and expected outputs, plus groups, assignments with deadlines, grading and grade export.&lt;/p&gt;

&lt;p&gt;Boolflow has a Circuit Check — a linter that finds unconnected inputs and floating outputs — and that is the whole of its verification story. If you are a teacher with thirty students, CircuitVerse solves your actual problem and Boolflow does not solve any part of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. No path to hardware.
&lt;/h3&gt;

&lt;p&gt;Logisim Evolution has FPGA board integration. Digital goes further: JEDEC output for GAL16v8 and GAL22v10, ATF150x CPLD support up to 128 macro-cells, direct support for BASYS3 and TinyFPGA BX boards, and it will drive ghdl or Icarus Verilog to simulate its own generated HDL.&lt;/p&gt;

&lt;p&gt;Boolflow emits Verilog and VHDL text and then wishes you luck. That is a real feature — it is not nothing to get synthesisable HDL out of a drawing — but "here is a file" and "here is your circuit running on a board" are different products.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Nobody has heard of it.
&lt;/h3&gt;

&lt;p&gt;Logisim Evolution has 7,482 stars and a decade of course syllabuses pointing at it. Digital has 5,943. Boolflow has me. When a tool is used by thousands of students, the bugs that matter have been found. Mine have not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Boolflow actually wins
&lt;/h2&gt;

&lt;p&gt;Shorter list, and I checked each one against the other nine rather than assuming.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zero friction, all the way down
&lt;/h3&gt;

&lt;p&gt;Logisim and Digital need Java. LogicCircuit and Deeds need Windows. Logicly needs $59. simulator.io and CircuitVerse need an account before your work survives a browser refresh. DigiSim's free tier caps you at 16 components.&lt;/p&gt;

&lt;p&gt;Boolflow: open the URL, place gates, save a JSON file. No install, no login, no tier, no component limit. Only Falstad matches that, and Falstad is an analog simulator you can coerce into doing logic.&lt;/p&gt;

&lt;p&gt;That sounds like a small thing until you watch someone try to use a simulator during a forty-minute class on a locked-down school laptop.&lt;/p&gt;

&lt;h3&gt;
  
  
  C++ export
&lt;/h3&gt;

&lt;p&gt;Nine other tools. Verilog appears in four of them, VHDL in three. C++ appears in none of them.&lt;/p&gt;

&lt;p&gt;I added it almost as a curiosity — take the drawn circuit, emit a plain C++ function that computes the same outputs, with an optional test harness. It turns out to be the bridge that people who write software but not hardware actually want. You do not have to learn an HDL to check that the thing you drew does what you think it does; you compile it and call it.&lt;/p&gt;

&lt;p&gt;I did not know this was unique until I went looking. It is now the feature I would lead with.&lt;/p&gt;

&lt;h3&gt;
  
  
  The theory sits next to the tool
&lt;/h3&gt;

&lt;p&gt;This was the whole point of v4 and it holds up. Seventeen articles, each one ending in a link that drops you into the editor to build the thing you just read about.&lt;/p&gt;

&lt;p&gt;Only two other tools in the table do this. Deeds does it well and is Windows-only 32-bit software from a university. DigiSim does it better than I do — 98 narrated lessons against my 17 articles — and puts most of them behind a subscription. CircuitVerse, the strongest browser tool by a distance, has documentation rather than a curriculum.&lt;/p&gt;

&lt;p&gt;So the niche is real. It is just narrower than I thought and I am not alone in it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trilingual, including the interface
&lt;/h3&gt;

&lt;p&gt;Boolflow's UI, its 17 articles and its per-element help all exist in English, Russian and German. Digital has seven interface languages but is desktop Java. DigiSim has seven languages and charges for most of the lessons. CircuitVerse is English-first. Logisim's teaching material is overwhelmingly English.&lt;/p&gt;

&lt;p&gt;If you are learning digital logic in German or Russian and you want the theory and the tool in your own language, in a browser, for free, the list of options is short.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Circuit Check
&lt;/h3&gt;

&lt;p&gt;I could not find an equivalent in any of the other nine as a first-class feature: a button that scans the schematic and tells you which inputs are unconnected and which outputs go nowhere.&lt;/p&gt;

&lt;p&gt;It is not clever. It is about eighty lines of code. It is also the single most common thing a beginner gets wrong, and every other tool makes them find it by wondering why nothing happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this changed on my roadmap
&lt;/h2&gt;

&lt;p&gt;I went in wanting to write a piece about how Boolflow compares. I came out with a re-ordered backlog, which is a better outcome.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sub-circuits.&lt;/strong&gt; Not "some day". Next. Everything else on this list is smaller than the gap between having them and not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Waveforms.&lt;/strong&gt; A timing panel for clocked circuits. The flip-flop and counter articles are currently writing cheques the editor cannot cash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test cases.&lt;/strong&gt; Digital's model — declare inputs and expected outputs, run, see pass or fail — is the right one, and it is also the missing half of "exercises with checkable answers", which I promised in the v4 post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything classroom-shaped: deliberately not.&lt;/strong&gt; Accounts, groups, grading, dashboards. CircuitVerse has done this properly for years and I am not going to do it better as one person. If you need it, use CircuitVerse. I would rather be the tool a single person opens at 2am than a worse version of a platform that already exists.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  So which one should you use?
&lt;/h2&gt;

&lt;p&gt;Honestly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Taking a university digital logic course?&lt;/strong&gt; Logisim Evolution. It is what your assignments assume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teaching a class of thirty?&lt;/strong&gt; CircuitVerse. Nothing else free gives you assignments and grading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pushing a design onto real hardware?&lt;/strong&gt; Digital. JEDEC, CPLD and board support that nothing else free comes close to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trying to understand what current actually does?&lt;/strong&gt; Falstad. Watch the dots move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want a paid, polished, teacher-first desktop app?&lt;/strong&gt; Logicly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want narrated lessons across digital, analog and relay logic and don't mind paying?&lt;/strong&gt; DigiSim.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want to open a tab right now, place a gate within ten seconds, generate a truth table, and read the theory in the same site in English, Russian or German — with no install, no account and no limit?&lt;/strong&gt; That one is &lt;a href="https://boolflow.site" rel="noopener noreferrer"&gt;Boolflow&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last sentence is a narrow claim. It is also, after two weeks of checking, a true one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tell me what I got wrong
&lt;/h2&gt;

&lt;p&gt;Genuinely — this is the part I want feedback on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you maintain or teach with one of these tools and I have misrepresented it, say so and I will correct the post. I tested each one myself but two weeks is two weeks.&lt;/li&gt;
&lt;li&gt;If you use a simulator I did not cover, name it. I deliberately left out Multisim, Proteus, Tinkercad and Wokwi as different categories, but I may have missed something in-category.&lt;/li&gt;
&lt;li&gt;Is my read on sub-circuits right? Is that the feature that would actually change whether you'd use Boolflow, or am I fixating on it because it is the most obvious hole?&lt;/li&gt;
&lt;li&gt;And the C++ export: is that useful to you, or is it a solution I invented for a problem nobody has?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Boolflow is at &lt;strong&gt;&lt;a href="https://boolflow.site" rel="noopener noreferrer"&gt;boolflow.site&lt;/a&gt;&lt;/strong&gt;, the articles are at &lt;strong&gt;&lt;a href="https://boolflow.site/articles" rel="noopener noreferrer"&gt;boolflow.site/articles&lt;/a&gt;&lt;/strong&gt;, and it is free with nothing to install.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>education</category>
      <category>computerscience</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
