<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
  <title>made by Niki — journal</title>
  <link>https://madebyniki.cc/journal/</link>
  <atom:link href="https://madebyniki.cc/feed.xml" rel="self" type="application/rss+xml"/>
  <description>Notes on programming and software development: invariants, small systems, and the parts nobody writes down.</description>
  <language>en</language>
  <lastBuildDate>Wed, 29 Jul 2026 12:00:00 +0000</lastBuildDate>
  <generator>publish.php — no dependencies</generator>
  <item>
    <title>invariants before tools</title>
    <link>https://madebyniki.cc/journal/invariants-before-tools/</link>
    <guid isPermaLink="true">https://madebyniki.cc/journal/invariants-before-tools/</guid>
    <pubDate>Wed, 29 Jul 2026 12:00:00 +0000</pubDate>
    <category>design</category>
    <category>method</category>
    <description><![CDATA[<p>Ask what a system is built with and you get a list: the language, the framework, the database, the queue. Ask what must never be untrue about it and most teams go quiet, then answer with something that turns out to be a preference rather than an invariant.</p>
<p>The difference is testable. An invariant is something that, if it stops being true, means the system is wrong even though nothing crashed. A preference is something that, if it stops being true, means somebody is annoyed.</p>
<h2>an example, small enough to hold</h2>
<p>A gallery site for a painter. The obvious requirements arrive first: pages for the works, an admin panel to add them, decent typography, a contact form.</p>
<p>None of those are invariants. These were:</p>
<ul><li>Withdrawing a work removes it everywhere at once: from the pages, the cache, the feed, etc.</li><li>Publishing is all-or-nothing. There is no state in which half the new exhibition is live.</li><li>The images outlive the site. If every line of code is deleted, the work is still on disk in its original resolution, in a directory a human can read.</li></ul>
<p>Notice what those three do to the design before a single technology is named. The first rules out any cache the publishing step cannot invalidate synchronously. In practice that means the publishing step must own the cache rather than politely notify it. The second gives you the atomic swap. The third says the images are not allowed to live only inside a CMS&#039;s storage abstraction, which quietly disqualifies most CMSes.</p>
<p>Three sentences, and the shape of the system is already decided. The stack is now a consequence: static output, a build step that owns the whole output directory, images stored as files under names a person chose.</p>
<h2>why this order and not the other</h2>
<p>The reverse order, tools first, feels faster because tools are concrete and invariants are not. You can install a framework this afternoon. You cannot install a decision about what must never be untrue.</p>
<p>But the cost arrives later and is charged with interest. When you pick the tool first, you inherit its invariants, and its invariants were written for someone else&#039;s problem. Every requirement of yours that conflicts with one of them becomes a workaround, and the workarounds accumulate until the system&#039;s real behaviour is documented nowhere except in the scar tissue.</p>
<p>I have watched this happen in software, and I have watched the identical thing happen with machinery on a farm. Buy the implement first and you spend the season adapting the field to it. Establish what the field requires and the implement chooses itself. The one that chooses itself is usually smaller, older and cheaper than the one you would have bought.</p>
<h2>how to find them</h2>
<p>Three questions, asked about the thing you are actually building rather than about systems in general:</p>
<ul><li>What would make a user say <em>this is broken</em> even though nothing threw an error? That is usually an invariant, stated in the negative.</li><li>What is the worst thing that can be true for one second? If the answer is <em>nothing much</em>, you have found a place where eventual consistency is genuinely fine, and those places are worth knowing too.</li><li>What must survive the deletion of this codebase? Data, formats, URLs. Those are invariants about the world, not about the program, and they are the ones people forget to write down.</li></ul>
<p>Write the answers as sentences, in a file, where the next person will find them. Tickets and tests come later. A test is an invariant made executable, which is only possible once the invariant has been written down.</p>
<h2>the part that keeps</h2>
<p>Tools change on a cycle of a few years and always will. The invariants of a gallery site have not changed since the first gallery: the withdrawn work stays withdrawn, the publish is all-or-nothing, the images outlive the code. They will read exactly the same in twenty years.</p>
<p>That asymmetry is the whole argument. Time spent on the layer that changes fastest is time you will spend again. Time spent on the layer that stays is the only kind that accumulates.</p>
<p class="result-line"><b>result</b>Find what must not change. Then make everything else cheap to change. In that order.</p>]]></description>
  </item>
  <item>
    <title>the rename is the deploy</title>
    <link>https://madebyniki.cc/journal/the-rename-is-the-deploy/</link>
    <guid isPermaLink="true">https://madebyniki.cc/journal/the-rename-is-the-deploy/</guid>
    <pubDate>Fri, 24 Jul 2026 12:00:00 +0000</pubDate>
    <category>deployment</category>
    <category>unix</category>
    <category>invariants</category>
    <description><![CDATA[<p>Watch a deploy closely and you will find a stretch of time — usually a few hundred milliseconds, sometimes a few minutes — during which the site is neither the version you had nor the version you wanted. Half the files are new. The CSS has been replaced but the HTML that needs it has not. Someone requests a page in exactly that moment and gets a document assembled from two different intentions.</p>
<p>Everyone knows this window exists. Almost nobody measures it, because the fix is assumed to be expensive: blue-green environments, a load balancer, a health check, a rollout controller, a yaml file describing the rollout controller.</p>
<p>It isn&#039;t expensive. It&#039;s one system call.</p>
<h2>the shape of the fix</h2>
<p>Build the new site somewhere else entirely. Then move a single pointer.</p>
<div class="codeblock" data-lang="php"><pre><code class="lang-php">$build = $buildsRoot.<span class="s">'/'</span>.date(<span class="s">'Ymd-His'</span>);
mkdir($build, <span class="s">0755</span>, <span class="k">true</span>);

<span class="c">/* … write every page into $build … */</span>

$tmp = <span class="s">"$root/.public.tmp"</span>;
@unlink($tmp);
symlink($build, $tmp);
rename($tmp, $publicLink);   <span class="c">/* before: old site. after: new site. */</span></code></pre></div>
<p><code>rename()</code> over an existing path is atomic on POSIX filesystems: <code>rename(2)</code> guarantees that a concurrent reader sees either the old entry or the new one, never a missing one and never a half-written one. The web server resolves <code>public</code> on every request. Before the rename it resolves to one finished build; after, to another finished build. There is no third state to be caught in.</p>
<p>The <code>.public.tmp</code> step matters more than it looks. <code>symlink()</code> fails if the target exists, so you cannot create the link directly over the live one — and <code>unlink()</code> followed by <code>symlink()</code> would open exactly the window we are trying to close, a window that is short but wide enough to serve a 404 to whoever is reading at that moment. Creating the link under a scratch name and renaming it into place keeps the whole operation to a single atomic step.</p>
<h2>what this buys, precisely</h2>
<dl class="case"><dt>problem</dt><dd>A deploy that is observable as a state — half old, half new.</dd><dt>invariants</dt><dd>A reader&#039;s request resolves to exactly one complete build. A failed build never becomes the live site.</dd><dt>decisions</dt><dd>Build to a fresh directory; publish by renaming a symlink; keep the last three builds on disk.</dd><dt>result</dt><dd>Deploys stop being events. There is nothing to schedule around and nothing to announce.</dd></dl>
<p>The second invariant comes free and is the one people notice later. If the build crashes halfway, it crashes in a directory nobody is serving. The live site does not know a deploy was attempted. You fix the error and run it again — no rollback procedure, because nothing rolled forward.</p>
<p>And rollback, when you do want it, is the same rename pointed at an older directory. Not a redeploy, not a revert commit, not a pipeline run: a rename. The last three builds are still sitting there.</p>
<h2>the objection</h2>
<p>The usual objection is that this only works for static output, and that real applications have databases and sessions and caches that cannot be swapped by renaming a directory.</p>
<p>That is true and it is also not an argument against doing it where it does work. A schema migration is genuinely hard; serving one consistent set of files is not. Most of what is deployed on any given day is the easy half wearing the difficulty of the hard half as an excuse.</p>
<h2>the smaller lesson</h2>
<p>The interesting part isn&#039;t the symlink. It&#039;s that the atomicity we wanted was already sitting in the filesystem, unused, while the industry built increasingly elaborate machinery to approximate it at a higher layer.</p>
<p>Before adding a system, it is worth asking which guarantee you actually need and how far down it already exists. Quite often the answer is: further down than you expected, and free.</p>
<p class="result-line"><b>result</b>This site publishes in one <code>rename()</code>. The window is not small. It is absent.</p>]]></description>
  </item>
</channel>
</rss>
