<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://blog.julik.nl/feed.atom.xml" rel="self" type="application/atom+xml" /><link href="https://blog.julik.nl/" rel="alternate" type="text/html" /><updated>2026-07-24T20:35:51+00:00</updated><id>https://blog.julik.nl/feed.atom.xml</id><title type="html">Julik Tarkhanov</title><subtitle>Engineering manager and developer</subtitle><entry><title type="html">Have less stuff</title><link href="https://blog.julik.nl/2026/07/less-stuff" rel="alternate" type="text/html" title="Have less stuff" /><published>2026-07-23T00:00:00+00:00</published><updated>2026-07-23T00:00:00+00:00</updated><id>https://blog.julik.nl/2026/07/less-stuff</id><content type="html" xml:base="https://blog.julik.nl/2026/07/less-stuff"><![CDATA[<p>When I joined <a href="https://cheddar.me">Cheddar</a> I was pleasantly surprised to find a sharp, mostly minimalist development culture which was a pleasant departure from the ZIRP-era “platform and team sprawl” that was in full bloom at WeTransfer.</p>

<p>The mostly-Shopify-inspired mindset - the Shopify of roughly 2012-2014, when Bouke and Tariq were there, long before the sharded-the-world Shopify of today - was extremely poignant and felt like a breath of fresh air:</p>

<ul>
  <li>Let’s not have services, at most - we’ll have 2</li>
  <li>Let’s not have Kubernetes and use GCP native VM scaling instead</li>
  <li>Let’s not have frontend bundling of any kind and rely on Rails builtins for any internal web plumbing</li>
</ul>

<p>Since then I took this approach to heart, and furthered it even when <a href="https://bou.ke/">Bouke</a> left for Monumental, and Emil chose for greener pastures as well. Time and time again, when we were faced with adding more systems and services to the stack, we resisted - and while I can’t prove we were right (we never ran the branch of history where we caved), not once did we later sit around wishing we had the extra stuff.</p>

<p>So what is it that made stuff my biggest enemy, and why should you consider joining the cause?</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>In this line of work that is about as close to proof as you get. The overall list would have been long, but we can note the key turning points and decisions:</p>

<ul>
  <li>Instead of furthering the service portfolio, we actually fused the Go server app into the Rails app as a set of endpoints and modules</li>
  <li>Instead of relying on Lambda running, we added responders to our monolith app</li>
  <li>Instead of using GCP PubSub for job queues, we migrated good_job into our own database-based queue adapter called <a href="https://github.com/cheddar-me/gouda">gouda</a> as solid_queue was still not available back then</li>
  <li>Instead of using Terraform for deployments we went on with simple scripts that call into the <code class="language-plaintext highlighter-rouge">gcloud</code> CLI to replace our fleet of machines</li>
  <li>Instead of using Kubernetes we engaged the GCP autoscaler and scaled using raw VMs, using custom metrics for storing queue depth and other important parameters</li>
  <li>Instead of creating a separate service for brand logos and matching, we embedded source-controlled YAML files and images into our base monolith image</li>
  <li>Instead of using gRPC we managed to maintain strong API contracts with our Flutter app by subscribing to just the protobufs - by using twirp.</li>
</ul>

<p>In general, at every turn of the crank - and at every addition to the system - we looked at the possible vehicles of implementing it and we asked <strong>what can we avoid using.</strong></p>

<h2 id="introduction-of-stuff-often-comes-from-people">Introduction of stuff often comes from people</h2>

<p>People have implicit identification with their work. And they do have a very natural fear of becoming redundant. It’s not often talked about in those terms, but a lot of Kubernetes evangelism comes from a fairly clear timeline started in the late 2010s, <a href="/2021/08/there-is-no-heroku-but-internal">which people tend not to like discussing:</a></p>

<ul>
  <li>A web product has one (or several) people who were old-school “sysadmins” of the system. They were responsible for maintaining and updating servers, (not)giving others root access, keeping things patched and managed</li>
  <li>Cloud comes along and a sweet package deal allows the company to move to the cloud, wholesale</li>
  <li>There are no more servers to manage. In theory, the new reality allows every developer to have root on machines. There is little custom infra to do, and the permissions management is standardized on the level of the cloud’s IAM model</li>
  <li>The sysadmins become worried: are we going to be made redundant? What is there for us to do? Where is our influence, our power, our job security going to go?</li>
  <li>Can we adopt a sufficiently complicated technology that justifies our employment, and usurp its management so that we are safe from layoffs (you don’t fire the person who has root access to systems your company relies on and that is hard to understand)?</li>
</ul>

<p>That last item is not a joke, and it’s not something I say just because of my anarchical views on power relationships in computing. I’ve literally heard “tomorrow this joint goes full-on Heroku, and what then remains for us to do here?” uttered not as a joke, but as a legitimate fear, an existential threat.</p>

<p>This mindset begets <em>stuff.</em> Stuff lets people keep power and control, it makes them feel useful, and it gives them a sense of security. The problem is, though, that:</p>

<ul>
  <li>Stuff tends to require maintenance</li>
  <li>Stuff makes things harder to change</li>
  <li>Stuff breaks in wild and interesting ways</li>
</ul>

<p>So: it is important to realize that furthering of <em>stuff</em> is something that happens when there is no engineering doctrine and people want to feel valued and safe. And we all like to put our imprint on the world, our little “dent in the universe” as they say.</p>

<p>And before I get too pleased with myself: wanting to be the one who <em>deleted</em> all the stuff, who ran the whole company on two services and a fistful of shell scripts, is its own little dent in the universe. Minimalism can be a flex and a personality just as readily as a Kubernetes cluster can. I am not exempt from wanting my imprint - I’ve just decided I want a small one.</p>

<h2 id="stuff-you-love-is-still-stuff">Stuff you love is still stuff</h2>

<p>There is a harder case than the sysadmin guarding his job, and honesty demands we name it: people become genuinely, tenderly attached to their stuff - not because it keeps them employed, but because they <em>built</em> it, or <em>chose</em> it, or <em>learned</em> it at some formative moment, and now it is woven into how their engineer identity. The event bus someone designed over a hard fortnight is not a line on an architecture diagram to them; it is a small monument. Asking them to delete it feels like being asked to admit the fortnight was wasted - or worse, that they were wrong, in public. It hurts! And pretending otherwise is precisely how we - ruthless minimalists - earn our reputation for being cold.</p>

<p>And the attachment is usually <em>well-earned.</em> The thing is frequently clever. It probably did solve the problem elegantly on the day it was born - the regex corpus was a nice idea, the sidecar was a neat trick, the bespoke tracing layer genuinely shows you things. None of that changes the arithmetic. Stuff you love still breaks at 3am, still needs upgrading, still has to be explained to the next hire, still becomes the thing “only Joost knows” - and Joost adoring it does not make Joost any less engaged in that clinch with company HR regarding his burnout leave.</p>

<p>So be mindful that when you come around to <em>cut stuff out</em> - your stuff is also on the line. Moreso: if it is you doing the cutting, the very stuff you hold the most dear should be the first subjected to scrutiny.</p>

<h2 id="the-many-horrors-of-stuff">The many horrors of stuff</h2>

<p>It’s not hard to enumerate, but I will go through the motions anyway - because while we all know that “stuff is bad” (because <a href="https://grugbrain.dev/">complexity bad</a>) we often do not like being reminded as to why exactly. So allow me the honor.</p>

<ul>
  <li>Stuff comes from vendors and gets sunset or changed at the vendor’s whim</li>
  <li>Stuff often includes opaque pricing, and turns out to cost a ton of money in the long run. A simple Postgres which is just like any other Postgres in your stack can be relocated to another vendor in an aternoon. A customized sharded Postgres with QoS proxies and custom tunnels in front of it, managed by a vendor where you do not have access to the logs and where even producing a dump is difficult - well, good luck moving off of that. And vendors <em>welcome</em> this: the more stuff you have with them, the more you depend on them.</li>
  <li>Stuff contains new things to learn, including undocumented things - which you still have to learn</li>
  <li>Stuff created dependency rings (we can’t deploy X before we deploy Y, but Y calls into an event bus which is listened to by X, and also Z manages permissions on the event bus) - the more stuff you allow, the more dependencies you need to manage</li>
  <li>Stuff begets <em>distributed</em> tracing - not because observability is bad (you want that in a monolith too), but because once a single request hops across seven services you can no longer watch it without stitching the hops back together by hand</li>
  <li>Stuff fails in parts. What is your uptime for the last quarter? Well, considering that 7 of the stuffs we use have 99.5, one of the stuffs has 99.9 but one endpoint on it times out in 30% of requests… well nobody knows really.</li>
  <li>Stuff creates information and management hoarding - the thing only Joost knows about, and Joost, we’ve established, is indisposed. Nobody else knows how to remove it or how to control it.</li>
  <li>Stuff makes large-scale migrations difficult. If you are beset with a system composed of 20 different stuffs (including vendor cloud products) and you realize that the architecture can be simplified, moving 2 units of stuff from system A to system B is usually much easier than moving all the 20.</li>
  <li>Stuff creates rifts between teams. If you have a team preferring tech X, and a team preferring tech Y, and the two teams are permitted to produce stuff - rest assured that they will, mostly out of spite and conviction - not because having stuff is useful for the business or for the tech doctrine of the firm, but because it allows them to not talk to each other. In reality, not talking to each other and furthering rifts by producing more stuff should be a punishable workplace transgression.</li>
</ul>

<h2 id="the-stuff-creep">The stuff creep</h2>

<p>Stuff is everywhere, and when you lower your guards: it will eat you alive, and start requiring substantial human resources to maintain. Plenty of sober teams still pick self-managed Kubernetes, and for real reasons - control, cost at scale, air-gapped environments, not wanting a cloud provider’s opinions baked into their cluster. While I have opinions, I would still like to lay out the bill if what they are <em>actually</em> subscribing to with this - because the bill is rarely shown at the point of sale. So: a simple chain of decisions to adopt stuff, a fairly popular one around 2018-2020:</p>

<ul>
  <li>To manage our deployments, let’s use Kubernetes!</li>
  <li>Well, with Kubernetes we need to decide which one we will use - managed from our cloud provider or self-managed. Let’s do self-managed! We’ll have more control!</li>
  <li>Oh, now we need to manage configurations. What do we have? Oh, let’s get Helm.</li>
  <li>Oh, and we also need to manage interconnections between our services inside Kubernetes, so let’s get istio.</li>
  <li>Oh, services need configuration - let’s use kustomize.</li>
  <li>Oh, kustomize templates credentials - let’s get a credentials management system</li>
  <li>Oh, credentials now do not update when we deploy our services - and besides, we should not allow services to reconfigure their own credentials - let’s write a custom tool that rolls credentials independently of deployments.</li>
  <li>Oh, credentials are now encrypted in files using some weird keypair setup - we need to have the private key of the keypair in our credential store in the cloud platform, and export it into Kubernetes which is self-managed</li>
  <li>Oh, now we need a tool to expose the private key to the pods - let’s write a sidecar pod which does credential import from our secrets store…</li>
  <li>Oh, now our instances authenticate under the same IAM machine role, whereas every pod is supposed to be running under the role appropriate for the service - more kustomize, and we also need to expose the role setup to our services</li>
  <li>Oh, the services use the AWS SDK to self-configure when running on AWS infrastructure - let’s get us an adapter that exposes machine info with appropriate roles into the pod - or, even better, let’s force all our services to explicitly authenticate using their assigned IAM keys!</li>
</ul>

<p>This snowball, once it gets rolling, is nigh impossible to stop - and, crucially, you don’t <em>just</em> backtrack out of all this tech you have so valiantly accrued. Now you need someone on your team to know and manage kustomize. To manage Helm. To upgrade Kubernetes (you picked a self-managed one, upgrades are on you now). To manage IAM bindings. To manage networking between pods and instances.</p>

<p>This is the worst kind of stuff proliferation, stuff that is effectively <em>forever.</em> Most of stuff, once applied, is forever.</p>

<h2 id="resisting-stuff">Resisting stuff</h2>

<ul>
  <li>Instead of Kubernetes we will run our workloads on autoscaling machine groups. A service specifies which machine configuration it needs and what templates it uses for networking and other capabilities. The service definition automatically pulls the correct Docker image with the correct tag and spins it up</li>
  <li>To serve credentials, we will specify encrypted environment variables which will be used to start Docker containers with our service inside the machine. The only environment variable will be the master key for the secrets</li>
  <li>Secrets will be stored encrypted in the service repositories. Secrets updated -&gt; service redeploys -&gt; secrets get loaded from the state they were in in the branch</li>
</ul>

<p>There, you have just saved yourself from a proliferation of stuff, and the only thing that you had to compromise on were the egos of your “right tool for the job” engineers.</p>

<p>And Kubernetes is just the most proverbially known cesspit of stuff. Simpler examples:</p>

<ul>
  <li>Want LaunchDarkly? Just use Rails and install <a href="https://github.com/flippercloud/flipper">Flipper</a> No, seriously - just install Flipper</li>
  <li>Want gRPC? See whether a properly encoded JSON API will do the job just as well. Just use Rails. Why? This frees you of <code class="language-plaintext highlighter-rouge">protoc</code>, of running 2 web servers in parallel, of forking bugs, of hung services, of installing more dependencies on developer machines, it shows the payloads in the Chrome dev tools.</li>
  <li>Want a separate service for matching bank transactions to merchants? Just get a corpus of regular expressions and store them in YAML files. Vet the YAML files for Norway (yes, you can absolutely do this). Generate unit tests from those YAML files. Do not create a service.</li>
  <li>Want sophisticated ETL? Don’t get sophisticated ETL, just bulk-export CSVs from your main DB on a fixed cadence.</li>
  <li>Want a build-time React-based interface setup? Write Rails helpers instead, dress them with vanilla CSS and a bit of Stimulus. Just use Rails. This allows you to escape <code class="language-plaintext highlighter-rouge">package.json</code>, <code class="language-plaintext highlighter-rouge">tsc</code>, deliberations on type signatures and disjoint union types, it gives you native backtraces in the browser…</li>
  <li>Want to have Lambdas? Just write HTTP handlers or functions inside your product. Use autoscaling, wrap your function calls with decent observability. This frees you from polyglot stacks, from distributed tracing, from debugging slow cold start, from debugging broken streaming, from having a separate versioning system for artifacts (remember deploying with ZIPs?)…</li>
</ul>

<p>A fair objection, and I’ll meet it head on: isn’t gouda stuff? Aren’t the deploy scripts, the autoscaler metrics, the corpus of regexes - aren’t those all <em>my</em> stuff, with the same bus factor and the same “only Joost knows” (well, Julik then - for the same amount) risk? Yes. Homegrown things are stuff too, and I won’t pretend otherwise. The difference I’ll defend is one of degree: a few hundred lines of Ruby that call <code class="language-plaintext highlighter-rouge">gcloud</code> are legible to anyone on the team in an afternoon, they carry no vendor, no separate control plane, no pricing page, and I can delete them the day they stop earning their keep. That is not the same beast as being married to a self-managed cluster. It’s not nothing, but it satisfies 2 criteria of acceptable mini-stuff:</p>

<ul>
  <li>It is small and easy to understand</li>
  <li>It is easy to remove (gouda can be replaced with solid_queue in an afternoon)</li>
</ul>

<h2 id="llms-permit-smaller-and-less-stuff">LLMs permit smaller (and less!) stuff</h2>

<p>For a long time the honest argument <em>for</em> stuff was cost of construction. Yes, you only needed a sliver of what Kafka does - ordered, durable-ish fan-out for a handful of topics - but writing even that sliver correctly was a fortnight you didn’t have, so you swallowed the whole broker, the whole operational manual, the zookeeper, the mother and her kitchensink - to get your 10%. Nowadays, that equation has quietly flipped. When an LLM can draft, test and harden the small version in an afternoon, the reason to adopt the big version - “building it ourselves is too expensive” - mostly evaporates, and what’s left is the part nobody wanted to admit: you were dragging along 90% of a system you were never going to use. You adopted it as <em>stuff</em> just because “those people know better”.</p>

<p>They don’t - often.</p>

<p>So build the subset. You can stand up a perfectly serviceable tiny Kafka <em>inside</em> your Rails app on top of Redis - or, honestly, on top of sqlite - the moment you know your own constraints: how many topics, what ordering you truly need, whether “at least once” is fine, how long you must retain. You stop reaching for SQS or GCP PubSub, because most of what you wanted from it is fan-out and retries you can bolt onto solid_queue as an extension and be done. At Cora we run a homegrown autoscaler for much the same reason - not because scaling is a solved-by-us miracle, but because these days you can just <em>do things,</em> and the thing we needed was a few hundred readable lines rather than a platform. The trick was never building the whole of X; it was knowing which tenth of X your problem actually touches, and now the wall between wanting that tenth and having it is thin enough to walk through.</p>

<h2 id="conclusion">Conclusion</h2>

<p>If you are in a position to manage the introduction of new stuff - if you are a VP of engineering, a director, a CTO: make new stuff <em>expensive.</em> Not forbidden - forbidding everything outright is just the mirror-image ideology, and I’ve no interest in trading one dogma for another - but expensive. Fight each new piece tooth and nail, make someone stand up and defend it, and put it through a process rigorous enough that only the stuff which genuinely earns its place survives contact with you. There is a scale, and a moment, where a given piece of stuff pays for itself - your job is to make quite sure this is that scale and that moment before you let it in.</p>

<p>Stuff bad. Complexity bad. Have less stuff.</p>

<p>Even if those wonderful people try to convince you otherwise.</p>

<h2 id="by-the-way">By the way</h2>

<p>If you have a Rails app and a nagging sense you’re oversubscribed on <em>stuff,</em> <a href="/hire-me">hit me up</a> and I’ll help you chop some of it away. Not going on staff makes me the ideal villain: pin all the grief on the consultant who swept in, took the knife to everyone’s darlings, and left. Your colleagues keep their dignity, you keep a smaller app, and I’ll gladly be the bad guy - it washes off by the next train home.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[When I joined Cheddar I was pleasantly surprised to find a sharp, mostly minimalist development culture which was a pleasant departure from the ZIRP-era “platform and team sprawl” that was in full bloom at WeTransfer. The mostly-Shopify-inspired mindset - the Shopify of roughly 2012-2014, when Bouke and Tariq were there, long before the sharded-the-world Shopify of today - was extremely poignant and felt like a breath of fresh air: Let’s not have services, at most - we’ll have 2 Let’s not have Kubernetes and use GCP native VM scaling instead Let’s not have frontend bundling of any kind and rely on Rails builtins for any internal web plumbing Since then I took this approach to heart, and furthered it even when Bouke left for Monumental, and Emil chose for greener pastures as well. Time and time again, when we were faced with adding more systems and services to the stack, we resisted - and while I can’t prove we were right (we never ran the branch of history where we caved), not once did we later sit around wishing we had the extra stuff. So what is it that made stuff my biggest enemy, and why should you consider joining the cause?]]></summary></entry><entry><title type="html">Asset pipelines: just say no</title><link href="https://blog.julik.nl/2026/05/just-say-no-to-asset-pipelines" rel="alternate" type="text/html" title="Asset pipelines: just say no" /><published>2026-05-06T00:00:00+00:00</published><updated>2026-05-06T00:00:00+00:00</updated><id>https://blog.julik.nl/2026/05/just-say-no-to-asset-pipelines</id><content type="html" xml:base="https://blog.julik.nl/2026/05/just-say-no-to-asset-pipelines"><![CDATA[<p>My good mate Yaroslav wrote about his new solution for bundling a complete Bun runtime inside a gem - by all means, <a href="https://yaroslav.io/posts/bundlebun-bundles-bun">give it a read.</a></p>

<p>A lot of points he makes are very sensible, but for a few months now I have decided that - for myself - I will not be using any frontend bundling nor JS runtime at all. Here’s how you can do the same.</p>

<h2 id="how-useful-is-an-asset-pipeline">How useful is an asset pipeline?</h2>

<p>There is, in fact, just one sensible use case for <em>needing</em> an asset pipeline, and it goes as follows.</p>

<ul>
  <li>You are running revision <code class="language-plaintext highlighter-rouge">abc0</code> on two machines, <code class="language-plaintext highlighter-rouge">box1</code> and <code class="language-plaintext highlighter-rouge">box2</code></li>
  <li>You make some glorious changes to your frontend code, and a new version gets prepared which requires those changes to be applied to the frontend assets your users get loaded</li>
  <li>You deploy revision <code class="language-plaintext highlighter-rouge">def1</code> with those changes. Then, because you sensibly deploy blue/green, the following happens:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">box1</code> gets the new version deployed, and a user hits it and loads the root page.</li>
      <li>The root page includes a reference to <code class="language-plaintext highlighter-rouge">scripts.js</code>, which is served from the same application. The browser shoots out a request for <code class="language-plaintext highlighter-rouge">scripts.js</code> as soon as it sees it while HTML is coming down on the wire</li>
      <li>The request for <code class="language-plaintext highlighter-rouge">scripts.js</code> ends up not on <code class="language-plaintext highlighter-rouge">box1</code>, but on <code class="language-plaintext highlighter-rouge">box2</code>. That box is still running your older version - the user gets their assets loaded for the older application revision, and gets a broken experience.</li>
    </ul>
  </li>
</ul>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>The venerable “asset fingerprinting” - the appending of the content-based hash to the filename (so that you get <code class="language-plaintext highlighter-rouge">scripts.d27e2763e.js</code>) actually exists to solve that problem, even though it is also pretty bulletproof for cache-busting. Thing is, though, to make it usable for the task, you need to change your deployment too:</p>

<ul>
  <li>First, you compile your assets for revision <code class="language-plaintext highlighter-rouge">def1</code> which produces <code class="language-plaintext highlighter-rouge">scripts.d27e2763e.js</code>. If your asset compilation is tuned just right, you do not inject any ENV variables from developer machines, you watch like a hawk that all the versions of everything match to-the-byte etc.</li>
  <li>This <code class="language-plaintext highlighter-rouge">d27e2763e</code> gets recorded in the asset manifest of your build <em>somehow</em> - remember, committing it is kinda stupid because it only makes sense for the build itself, and content-addressable identifiers can be recomputed when needed. In practice it produces what used to be called a “manifest” (in Sprockets parlance) which maps “logical paths” - which are not paths but filenames (because <a href="https://blog.julik.nl/2016/10/sprockets">Sprockets was shit and still is</a>) - to those filenames/paths with digest fingerprints added. That manifest MUST get deployed together with your build (but usually - not committed, on which - later)</li>
  <li>You preupload <code class="language-plaintext highlighter-rouge">scripts.d27e2763e.js</code> and other files somewhere. That somewhere should be a CDN-fronted storage system (like an S3 bucket) which is not served from your actual application deployment (<code class="language-plaintext highlighter-rouge">box1</code> or <code class="language-plaintext highlighter-rouge">box2</code>) - it has to be available elsewhere before the first request hits <code class="language-plaintext highlighter-rouge">box1</code> or <code class="language-plaintext highlighter-rouge">box2</code> for revision <code class="language-plaintext highlighter-rouge">def1</code></li>
  <li><em>Then</em> you do your release and roll <code class="language-plaintext highlighter-rouge">box1</code>. The URL in the HTML template served from <code class="language-plaintext highlighter-rouge">box1</code> references <code class="language-plaintext highlighter-rouge">scripts.d27e2763e.js</code></li>
  <li>Because this digest can only ever match the assets built for revision <code class="language-plaintext highlighter-rouge">def1</code>, it is not possible that the end user will get the assets from revision <code class="language-plaintext highlighter-rouge">abc0</code> loaded.</li>
</ul>

<p>Splendid, isn’t it? Indeed, it is. It solves a very specific problem which happens if your deployment fleet is large, and that problem is not a fabrication - it’s an actual issue that apps stumble with. However, let’s review what you need to put all of that into motion.</p>

<ul>
  <li>You now have a mapping from <code class="language-plaintext highlighter-rouge">scripts.js</code> -&gt; <code class="language-plaintext highlighter-rouge">scripts.&lt;digest&gt;.js</code>. Every load, every <code class="language-plaintext highlighter-rouge">src</code>, every <code class="language-plaintext highlighter-rouge">href</code>, every <code class="language-plaintext highlighter-rouge">import</code> must take this into account</li>
  <li>That mapping is not committed with your code - it needs to be produced before deployment, in a coordinated fashion</li>
  <li>That mapping depends on all the versions of all the ancillary tools you use as part of your build (Node, every node module version, all the injected variables…)</li>
  <li>That mapping <em>also</em> depends on the correct configuration of the asset pipeline in every single Rails environment. Ever had <code class="language-plaintext highlighter-rouge">application.assets</code> be <code class="language-plaintext highlighter-rouge">nil</code>? If you haven’t used Sprockets - you are lucky, but I can assure you it is an infuriating affordance breach, and I am not happy how many times I have hit it</li>
</ul>

<p>Here’s the spicy bit: <strong>everything</strong> Yaroslav mentions is achievable without an asset pipeline as long as you do not need to digest your assets. Which is exactly what I picked for my own apps for now.</p>

<h2 id="why-propshaftimportmaps-is-not-it">Why propshaft/importmaps is not “it”</h2>

<p>Importmaps seem nice on the surface. What they do is that they map known JS modules to URLs, nothing more. For example, if you are using a lib called <code class="language-plaintext highlighter-rouge">shponk</code>, you can reference it inside your own application like so:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"importmap"</span><span class="nt">&gt;</span>
  <span class="p">{</span>
    <span class="dl">"</span><span class="s2">imports</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
      <span class="dl">"</span><span class="s2">@shponk</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/js/shponk.js</span><span class="dl">"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>or reference the library on a CDN somewhere:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"importmap"</span><span class="nt">&gt;</span>
  <span class="p">{</span>
    <span class="dl">"</span><span class="s2">imports</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
      <span class="dl">"</span><span class="s2">@shponk</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">https://nopkg.example/shponk@12.0.2/bundle.js</span><span class="dl">"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">@</code> in the name is just a convention most people use to signal that “this module is resolved using some magical facility instead of being just loaded via a path”. In practice you can omit it. Then, in an importmap-capable browser, you can do:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span><span class="nx">doShponk</span><span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@shponk</span><span class="dl">"</span><span class="p">;</span>
</code></pre></div></div>

<p>and you should be good to go. There’s just a little snag with this:</p>

<blockquote>
  <p>You now have a “manifest” you need to manually update and nourish at every whiff</p>
</blockquote>

<p>No, really. Added a <code class="language-plaintext highlighter-rouge">helpers.js</code>? Don’t forget to add <code class="language-plaintext highlighter-rouge">pin "helpers", to: "helpers.js"</code> to <code class="language-plaintext highlighter-rouge">config/importmap.rb</code>. Oh, <code class="language-plaintext highlighter-rouge">helpers</code> has become a directory? No, you won’t just import a barrel file, you need to do <code class="language-plaintext highlighter-rouge">pin_all_from 'app/javascript/helpers', under: 'helpers'</code> (and remember to call <code class="language-plaintext highlighter-rouge">enable_integrity!</code> at the top of the file if you want SRI to actually do something)… it just goes on and on and on.</p>

<p>Basically, what should be <code class="language-plaintext highlighter-rouge">import {doThing} from "./helpers.js"</code> now becomes a ceremonial ritual. It becomes <em>shit work.</em> <em>Shit work</em> are actions that nobody enjoys, nobody benefits from, nobody likes - and everybody suffers from after. Adding a source file to your codebase should not be a reelection-of-the-Pope type affair - it’s ridiculous.</p>

<p>In practice, even though importmaps do work - actually using them becomes the new <code class="language-plaintext highlighter-rouge">#import self</code> from Sprockets. It’s “logical paths” all over.</p>

<p>I took a brief stock of that and found it not only lacking - I found it abysmal for what Rails aspires to be. No way in hell I am using all of that after being subjected to webpacker, webpack, vite, esbuild and a whole legion of various frontend regurgitation tools. It just doesn’t bring <em>any</em> value - except for previously mentioned content hashing.</p>

<h2 id="what-could-we-not-do">What could we not do?</h2>

<p>The answer is surprisingly simple. Here’s how it <em>should</em> work:</p>

<ul>
  <li>You add <code class="language-plaintext highlighter-rouge">app/javascript/helpers.js</code> into your app.</li>
  <li>In your app code, which is either another JS module or a <code class="language-plaintext highlighter-rouge">script</code> element, you <code class="language-plaintext highlighter-rouge">import</code> it by its root-relative URL. <em>That’s IT.</em></li>
</ul>

<p>The only thing that needs to happen is that your app/javascript directory gets copied as-is to your <code class="language-plaintext highlighter-rouge">public/</code> directory on deploy, or that there is some Rails/Rack passthrough for that directory.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"module"</span><span class="nt">&gt;</span>
  <span class="k">import</span> <span class="p">{</span><span class="nx">doThing</span><span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">/js/helpers.js</span><span class="dl">"</span><span class="p">;</span>

  <span class="nb">window</span><span class="p">.</span><span class="nx">myApplication</span> <span class="o">=</span> <span class="p">{</span><span class="nx">doThing</span><span class="p">,</span> <span class="nx">doSomethingElse</span><span class="p">};</span> <span class="c1">// Module-land escape</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>That’s <em>it.</em> No, I mean - <em>that’s it.</em> Imagine we add another helper file, like <code class="language-plaintext highlighter-rouge">geom.js</code>? Here is <em>all</em> that should be required:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"module"</span><span class="nt">&gt;</span>
  <span class="k">import</span> <span class="p">{</span><span class="nx">doThing</span><span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">/js/helpers.js</span><span class="dl">"</span><span class="p">;</span>
  <span class="k">import</span> <span class="p">{</span><span class="nx">arc</span><span class="p">,</span> <span class="nx">circle</span><span class="p">,</span> <span class="nx">rect</span><span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">/js/geom.js</span><span class="dl">"</span><span class="p">;</span>

  <span class="nb">window</span><span class="p">.</span><span class="nx">myApplication</span> <span class="o">=</span> <span class="p">{</span><span class="nx">doThing</span><span class="p">,</span> <span class="nx">doSomethingElse</span><span class="p">,</span> <span class="nx">arc</span><span class="p">,</span> <span class="nx">circle</span><span class="p">,</span> <span class="nx">rect</span><span class="p">};</span> <span class="c1">// Module-land escape</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>Nothing else. You should not have to run Rake tasks to add it. You should not have to edit some <code class="language-plaintext highlighter-rouge">importmaps.rb</code> file, you should not encounter “relative paths are not supported” errors - nothing of that.</p>

<p>Here is specifically what breaks if we try, though. These points are mentioned by Yaroslav in his article but I will reiterate.</p>

<ul>
  <li>Because those modules are discovered as JS arrives and gets parsed, we can’t load them in parallel - they have to be predeclared somewhere. <code class="language-plaintext highlighter-rouge">a.js</code> imports <code class="language-plaintext highlighter-rouge">b.js</code> - the browser has to load <code class="language-plaintext highlighter-rouge">a.js</code> to find out it also needs <code class="language-plaintext highlighter-rouge">b.js</code>, <code class="language-plaintext highlighter-rouge">b.js</code> loads something else - and before you know it your page’s first meaningful paint is delayed several seconds. Valid.</li>
  <li>You load <code class="language-plaintext highlighter-rouge">geom.js</code>. Remember how we have <code class="language-plaintext highlighter-rouge">box1</code> and <code class="language-plaintext highlighter-rouge">box2</code>? If you do not pin the user’s session to a particular host, they can load the page from <code class="language-plaintext highlighter-rouge">box1</code>, and then try to load <code class="language-plaintext highlighter-rouge">geom.js</code> from <code class="language-plaintext highlighter-rouge">box2</code>. It will load, but will be an old version. Or not load, because it is not there.</li>
  <li><code class="language-plaintext highlighter-rouge">geom.js</code> may well load, but due to misconfigured caching - or insufficient brutality on the part of the browser - get served stale.</li>
</ul>

<p>Once again, <em>the only meaningful win</em> from all this stupid “run command just to add a file” bureaucracy are file digests. The rest are losses and schlepp.</p>

<p>But let’s contemplate: what if we try to solve those problems, how far could we take the idea?</p>

<h2 id="a-note-on-stale-assets">A note on stale assets</h2>

<p>Up to a decade ago, people would only serve static assets through a CDN. CDNs were seen as something Expensive™, Hard-to-configure™, Very-Enterprise™… We would only place things like images, CSS files, large videos and so on on CDNs. These times are long past us.</p>

<p>Right now, CDNs are <em>proxies.</em> And if you care at all about your app’s performance - you will be using some kind of caching proxy in front of you. The simplest one is actually Thruster - which ships in the box with Rails these days. Now, I am <em>very</em> disappointed that 37s would ship those Go proxies incessantly - proving the naysayers right that you “just don’t write those tools in Ruby”, which is utter bullshit - but that aside: you will usually have <em>both</em> an HTTP/2 multiplexor <em>and</em> a caching proxy in front of your Rails app. <strong>Use it!</strong></p>

<p>This means that we <em>can</em> actually serve our assets from the Rails app itself, provided that we tag it with correct <a href="/2025/06/getting-conditionally">cache control headers.</a> It works fine, really! Nothing prevents you from computing a SHA for your files, and you won’t save 9 years of compute by carefully doing it just-once-and-at-build-time. Again: it’s peanuts optimization that creates ceremony, it’s a generator of chicken-and-egg problems.</p>

<p>So, the stale assets problem <em>can</em> be alleviated: tell the server to revalidate after a meaningful amount of time, serve a stable and content-derived ETag, make your URL reflect the revision <em>in some way</em> - but it doesn’t have to be precomputed. Again, more on that later.</p>

<h2 id="solving-the-waterfall">Solving the waterfall</h2>

<p>The waterfall is trickier - indeed, there is no way to get rid of it without having a <code class="language-plaintext highlighter-rouge">modulepreload</code> of some kind. But if we want to have a <code class="language-plaintext highlighter-rouge">modulepreload</code> - we sure have to have some build step, which scans our <code class="language-plaintext highlighter-rouge">importmaps.rb</code> and then… right?</p>

<p><strong>Nothing of that.</strong> Here is what we can do: those assets are text, sheesh! You can actually process them <em>as text</em> just as they get requested. How about we load the file, scan it for everything it may try to import, and transform that list into a <code class="language-plaintext highlighter-rouge">modulepreload</code>? For example, here is how the automatic module preload directive looks in <code class="language-plaintext highlighter-rouge">geneva_drive_admin</code>:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"modulepreload"</span> <span class="na">href=</span><span class="s">"/admin/workflows/assets/anicon.js?v=aee2"</span><span class="nt">&gt;</span>
<span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"modulepreload"</span> <span class="na">href=</span><span class="s">"/admin/workflows/assets/polling.js?v=aee2"</span><span class="nt">&gt;</span>
<span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"modulepreload"</span> <span class="na">href=</span><span class="s">"/admin/workflows/assets/step_execution_timeline/index.js?v=aee2"</span><span class="nt">&gt;</span>
<span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"modulepreload"</span> <span class="na">href=</span><span class="s">"/admin/workflows/assets/step_execution_timeline/rendering/rendering.js?v=aee2"</span><span class="nt">&gt;</span>
<span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"modulepreload"</span> <span class="na">href=</span><span class="s">"/admin/workflows/assets/step_execution_timeline/test_cases.js?v=aee2"</span><span class="nt">&gt;</span>
<span class="c">&lt;!-- ... more of the same --&gt;</span>
<span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"modulepreload"</span> <span class="na">href=</span><span class="s">"/admin/workflows/assets/workflow_polling/queue.js?v=aee2"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>This should not require <em>any</em> regeneration, building, compilation… it can just be done at page load. And placed where it belongs - <em>in the Rails application cache.</em> Stuff it there, tag it with the Rails application git SHA - and never think about it again. It will be reasonably fast, trust me.</p>

<p>How fast? This set of module preloads takes <code class="language-plaintext highlighter-rouge">3.4ms</code> on average on my laptop (which is fast). There is no parallelization - I could add some to add a queue of scans as files get discovered, but - on the other hand - if you just <em>glob</em> for all the files you have, and you assume they are a module (which you can do reasonably well by grepping the file for <code class="language-plaintext highlighter-rouge">/export/</code> or similar or using the <code class="language-plaintext highlighter-rouge">.mjs</code> filename extension). So, let’s do some preloading:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">modulepreload_tags</span>
  <span class="n">paths</span> <span class="o">=</span> <span class="no">ApplicationHelper</span><span class="p">.</span><span class="nf">js_module_paths</span><span class="p">(</span><span class="n">engine_version_tag</span><span class="p">)</span>
  <span class="n">safe_join</span><span class="p">(</span><span class="n">paths</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">path</span><span class="o">|</span> <span class="n">tag</span><span class="p">.</span><span class="nf">link</span><span class="p">(</span><span class="ss">rel: </span><span class="s2">"m odulepreload"</span><span class="p">,</span> <span class="ss">href: </span><span class="n">dirty_admin_asset_path</span><span class="p">(</span><span class="n">path</span><span class="p">))</span> <span class="p">},</span> <span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
<span class="k">end</span>

<span class="c1"># Scan public/ for JS files that are ES modules (contain top-level import/export).</span>
<span class="c1"># Cached in production (version tag is stable); recomputed every request in development.</span>
<span class="vi">@js_module_paths_mutex</span> <span class="o">=</span> <span class="no">Mutex</span><span class="p">.</span><span class="nf">new</span>

<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">js_module_paths</span><span class="p">(</span><span class="n">version_tag</span><span class="p">)</span>
  <span class="vi">@js_module_paths_mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
    <span class="k">return</span> <span class="vi">@js_module_paths</span> <span class="k">if</span> <span class="vi">@js_module_paths_version</span> <span class="o">==</span> <span class="n">version_tag</span>

    <span class="n">public_dir</span> <span class="o">=</span> <span class="no">Engine</span><span class="p">.</span><span class="nf">root</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">"public"</span><span class="p">)</span>
    <span class="n">paths</span> <span class="o">=</span> <span class="no">Dir</span><span class="p">[</span><span class="n">public_dir</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">"**/*.{js,mjs}"</span><span class="p">)].</span><span class="nf">filter_map</span> <span class="p">{</span> <span class="o">|</span><span class="n">abs</span><span class="o">|</span>
      <span class="k">next</span> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">foreach</span><span class="p">(</span><span class="n">abs</span><span class="p">).</span><span class="nf">any?</span> <span class="p">{</span> <span class="o">|</span><span class="n">line</span><span class="o">|</span> <span class="n">line</span><span class="p">.</span><span class="nf">match?</span><span class="p">(</span><span class="sr">/\A\s*(import|export)\s/</span><span class="p">)</span> <span class="p">}</span>
      <span class="s2">"/</span><span class="si">#{</span><span class="no">Pathname</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">abs</span><span class="p">).</span><span class="nf">relative_path_from</span><span class="p">(</span><span class="n">public_dir</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span>
    <span class="p">}.</span><span class="nf">sort</span>

    <span class="vi">@js_module_paths</span> <span class="o">=</span> <span class="n">paths</span>
    <span class="vi">@js_module_paths_version</span> <span class="o">=</span> <span class="n">version_tag</span>
    <span class="n">paths</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Note that we do not even use the Rails cache here - just an in-memory array. Because we don’t need to rename anything - and because it doesn’t seem the order of those preloads makes much difference - all we need to do is just tell the browser “here is a file and we know it is a module”. Done.</p>

<h2 id="solving-the-cache-busting">Solving the cache busting</h2>

<p>Now, cache busting is trickier. I look at it like this: if we use a decent fronting cache (or a CDN) it should support caching with query strings. And a query string can have anything embedded in it at generation time, and that embedded value will force the cache key to change accordingly. This is why the <code class="language-plaintext highlighter-rouge">modulepreload</code> links have this <code class="language-plaintext highlighter-rouge">v=</code> parameter, see? Here is how it gets computed:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Engine</span> <span class="o">&lt;</span> <span class="o">::</span><span class="no">Rails</span><span class="o">::</span><span class="no">Engine</span>
  <span class="c1"># isolate_namespace GenevaDrive::Admin</span>

  <span class="c1"># Short version tag for cache busting across the engine.</span>
  <span class="c1"># In development: timestamp for instant invalidation</span>
  <span class="c1"># In production: derived from APP_REVISION env var or Gemfile.lock digest</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">version_tag</span>
    <span class="vi">@version_tag</span> <span class="o">||=</span> <span class="k">if</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">env</span><span class="p">.</span><span class="nf">development?</span>
      <span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">.</span><span class="nf">utc</span><span class="p">.</span><span class="nf">strftime</span><span class="p">(</span><span class="s2">"%Y%m%d%H%M%S"</span><span class="p">)</span>
    <span class="k">elsif</span> <span class="p">(</span><span class="n">app_revision</span> <span class="o">=</span> <span class="no">ENV</span><span class="p">[</span><span class="s2">"APP_REVISION"</span><span class="p">]).</span><span class="nf">present?</span>
      <span class="no">Digest</span><span class="o">::</span><span class="no">SHA1</span><span class="p">.</span><span class="nf">hexdigest</span><span class="p">(</span><span class="n">app_revision</span><span class="p">)[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">4</span><span class="p">]</span>
    <span class="k">else</span>
      <span class="n">gemfile_lock_path</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">root</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">"Gemfile.lock"</span><span class="p">)</span>
      <span class="k">if</span> <span class="n">gemfile_lock_path</span><span class="p">.</span><span class="nf">exist?</span>
        <span class="no">Digest</span><span class="o">::</span><span class="no">SHA1</span><span class="p">.</span><span class="nf">file</span><span class="p">(</span><span class="n">gemfile_lock_path</span><span class="p">).</span><span class="nf">hexdigest</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">4</span><span class="p">]</span>
      <span class="k">else</span>
        <span class="p">(</span><span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">.</span><span class="nf">utc</span><span class="p">.</span><span class="nf">to_i</span> <span class="o">/</span> <span class="mi">300</span><span class="p">).</span><span class="nf">to_s</span><span class="p">(</span><span class="mi">16</span><span class="p">)</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This needs a bit of an explainer. See, the geneva_drive_admin is a Rails engine. It can have its own versioning, of course, but it also - most likely - depends on the version of the host application it gets mounted into. That is important: if the host application changes how it does authentication, for example, or does something to its asset delivery, or changes a major version of ActionController (actionpack) - it is prudent to flush the cache for our assets. If the app does not keep track of its revision, we can fallback to the host app’s <code class="language-plaintext highlighter-rouge">Gemfile.lock</code> - it lists all the dependencies, so it makes for a very nice cache marker, and so on.</p>

<p><code class="language-plaintext highlighter-rouge">APP_REVISION</code> is a conventional env var that I add to all the apps I work on or deploy - modern Rails has a config parameter for this called <code class="language-plaintext highlighter-rouge">Rails.app.revision</code>.</p>

<p>That isn’t all, however. The URL of the “root” asset (the main module we load, for example) is just one piece of the puzzle. If we have <code class="language-plaintext highlighter-rouge">a.js</code> and we request it with <code class="language-plaintext highlighter-rouge">a.js?rev=1234</code>, it may - in turn - be importing <code class="language-plaintext highlighter-rouge">b.js</code>. To make this work we would also need to append <code class="language-plaintext highlighter-rouge">?rev=1234</code> to the request for <code class="language-plaintext highlighter-rouge">b.js</code> - but the URL is effectively embedded in <code class="language-plaintext highlighter-rouge">a.js</code> at this point. How do we solve it?</p>

<p>Well… by doing an extremely dirty rewrite on our CSS and JS on the fly, right as we return it to the browser. This is accomplished by using the following methods:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c1"># Matches quoted strings that look like relative/absolute JS import paths.</span>
<span class="c1"># Handles ./  ../  /  but NOT protocol-relative //</span>
<span class="c1"># $1 = quote char, $2 = path including extension</span>
<span class="no">JS_IMPORT_RE</span> <span class="o">=</span> <span class="sr">/(["'])(\.{0,2}\/(?!\/)[^"']*\.(?:js|mjs|es))\1/</span>

<span class="c1"># Matches url() in CSS with relative/absolute paths.</span>
<span class="c1"># Handles url(./path), url("./path"), url('../path'), url(/path)</span>
<span class="c1"># but NOT url(data:...), url(https://...), url(//...)</span>
<span class="c1"># $1 = opening (quote or empty), $2 = path, $3 = closing (quote or empty)</span>
<span class="no">CSS_URL_RE</span> <span class="o">=</span> <span class="sr">/url\((\s*["']?)(\.{0,2}\/(?!\/)[^)"']*?)(\s*["']?\s*)\)/</span>

<span class="k">def</span> <span class="nf">rewrite_js_imports</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">version_tag</span><span class="p">)</span>
  <span class="n">source</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="no">JS_IMPORT_RE</span><span class="p">)</span> <span class="k">do</span>
    <span class="s2">"</span><span class="si">#{</span><span class="vg">$1</span><span class="si">}#{</span><span class="vg">$2</span><span class="si">}</span><span class="s2">?v=</span><span class="si">#{</span><span class="n">version_tag</span><span class="si">}#{</span><span class="vg">$1</span><span class="si">}</span><span class="s2">"</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">rewrite_css_urls</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">version_tag</span><span class="p">)</span>
  <span class="n">source</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="no">CSS_URL_RE</span><span class="p">)</span> <span class="k">do</span>
    <span class="s2">"url(</span><span class="si">#{</span><span class="vg">$1</span><span class="si">}#{</span><span class="vg">$2</span><span class="si">}</span><span class="s2">?v=</span><span class="si">#{</span><span class="n">version_tag</span><span class="si">}#{</span><span class="vg">$3</span><span class="si">}</span><span class="s2">)"</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Yes, it’s ugly and it will probably break in some edge cases. It’s also glorious and works just fine for me. Yes, it does change the source code of the JS files and CSS that we serve, but it does so in a minimal way and the changes are always in the scope of one source code line. Line numbers won’t slide, and so on - and since we deliver raw code to the browser - we don’t have any source maps sliding about either!</p>

<p>The final piece of the puzzle is a Rails controller that allows us to serve our assets with the rewriting applied, which can be found <a href="https://gist.github.com/julik/f87ec705ebc25f489390ab12579a8e89">in this Gist.</a></p>

<p>So…</p>

<h2 id="back-to-other-points">Back to other points</h2>

<p>Yes, we will lose brotli deduplication compression. That would - for geneva_drive_admin - make a difference of 15% perhaps? since the code is not very repetitive. Yes, we will lose tree shaking - which I need none of, because if I have some JS code in my system - it is there for a reason and does not need to be shaken anywhere. We will lose JSX compilation - I want none of React and none of JSX in my own apps, ever. We lose TypeScript, but I don’t use it - I just care about fast delivery of as many JS files as I consider useful.</p>

<h2 id="so-what-do-we-write">So what <em>do</em> we write?</h2>

<p>Once the bundler is gone, what do you actually write? Because JSX needs the bundler. TSX needs the bundler. Svelte needs the bundler. Vue SFCs need the bundler. The reason all of these need a build step is plain enough - <em>they are not what the browser speaks.</em> They are <em>not of the web</em> - they are significant and flamboyant inventions, and they have their place - but they are not native. They get compiled down to HTML, CSS and JS at build time, and the cost of that compilation is a pipeline akin to Sprockets/propshaft/you name it.</p>

<p>Here is the what I sincerely recommend instead. It’s not fashionable, but effective: <a href="https://justfuckingusehtml.com/">write HTML.</a> Write CSS. Write JS. The browser speaks all three of them, and it has gotten <em>glorious</em> at it. Want a modal? <code class="language-plaintext highlighter-rouge">&lt;dialog&gt;</code>. Want a popover? <code class="language-plaintext highlighter-rouge">popover</code> attribute. Want sticky positioning, container queries, anchor positioning, view transitions, scroll-driven animations, <code class="language-plaintext highlighter-rouge">:has()</code>? It is all there for you. No <code class="language-plaintext highlighter-rouge">npm install</code> required. Want declarative state-driven UI? Server-render the HTML, swap fragments with Turbo. Need a sprinkle of behavior on top? Stimulus controllers, written as plain ES modules sitting in <code class="language-plaintext highlighter-rouge">public/js/controllers/</code>, registered once. Want a signals-based state store? grab your LLM of choice and write one, it will likely be 100 lines or less.</p>

<p>For the cases where Stimulus is not enough - and they do exist - there are web components. Custom elements ship in the browser. They have lifecycle callbacks, encapsulated DOM, the lot. And they will outlive every framework that is fashionable right now, because they are part of the platform you are already standing on.</p>

<p>And here is the bit that keeps getting lost: a seasoned Rails developer worth their salt <em>already knows all of this</em>. ERB, HTML, CSS, a bit of JS. There is no parallel runtime to learn, no state container to reach for, no router to configure, no hydration to debug, no framework migration to put on the roadmap for Q3. The platform <em>is</em> your framework.</p>

<p>A React codebase from 2018 is hostile to its 2026 maintainer - class components, deprecated lifecycles, the hooks rewrite, three replaced state libraries, two replaced routers. An HTML page from 2018 with a Stimulus controller dropped next to it still works, pretty much unchanged. Do you want to be creating a codebase that will be hostile to its 2034 maintainer?</p>

<p>Vanilla things work. Always bet on vanilla.</p>

<h2 id="recap">Recap</h2>

<p>What did we lose by implementing this horrible contraption? The impossibility of deploying incorrect assets to an incorrect revision, for sure. Which was - to begin with - <em>ONLY</em> possible to implement if you store and serve your assets from a different spot than your work machines, and coordinate your deploys carefully. We lost a bit of compression (which I doubt would be very useful for most things I do) and the opportunity to <em>consume the glorious JS ecosystem of libraries</em> - which I don’t find a lot of use for. If I do, however, I tend to either bundle those modules or use them from a CDN.</p>

<p>Here is what we gained: we don’t have to think about all this ceremonial garbage, ever again.</p>

<p>And good riddance. Just drop that <code class="language-plaintext highlighter-rouge">.js</code> file where it belongs and do the UI that you care about.</p>

<p><em>P.S.</em> Should you still find yourself with a strong desire to bundle - use <code class="language-plaintext highlighter-rouge">esbuild</code>. Not Bun. Not anything that wraps Bun. Here is why.</p>

<p>Tailwind v4’s standalone CLI is shipped as a <code class="language-plaintext highlighter-rouge">bun build --compile</code> artifact - meaning every Tailwind v4 user is running Bun, whether they signed up for that or not. And Bun, at some point, unilaterally decided that Intel Macs without AVX2 were no longer worth supporting. No prebuilt binary for Darwin x86_64 below that instruction set, full stop. No fallback. No emulation either, because AVX2 is a CPU feature - the silicon either has it or it does not. On Linux the maintainers walked it back after enough outrage from users; on Darwin, your Intel laptop is still beneath them. Your computer from 2013 is not good enough to edit CSS, and it truly took the frontend community something to get to this.</p>

<p>So when Bun decided your machine no longer mattered, Tailwind decided the same thing - downstream, transitively, without anyone ever telling you. Bundlebun bundles the same Bun, so it inherits the same gate. <a href="/2025/03/a-little-adventure-in-modern-frontend">I have already been bitten by exactly this</a>, and the answer is not to add another sophisticated wrapper to soften the blow. The answer is a shorter dependency chain, made of boring tools that ship as one static binary, written a long while ago and known to be of supreme quality.</p>

<p>I trust ev, and I no longer trust Jarred. ev is cool.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[My good mate Yaroslav wrote about his new solution for bundling a complete Bun runtime inside a gem - by all means, give it a read. A lot of points he makes are very sensible, but for a few months now I have decided that - for myself - I will not be using any frontend bundling nor JS runtime at all. Here’s how you can do the same. How useful is an asset pipeline? There is, in fact, just one sensible use case for needing an asset pipeline, and it goes as follows. You are running revision abc0 on two machines, box1 and box2 You make some glorious changes to your frontend code, and a new version gets prepared which requires those changes to be applied to the frontend assets your users get loaded You deploy revision def1 with those changes. Then, because you sensibly deploy blue/green, the following happens: box1 gets the new version deployed, and a user hits it and loads the root page. The root page includes a reference to scripts.js, which is served from the same application. The browser shoots out a request for scripts.js as soon as it sees it while HTML is coming down on the wire The request for scripts.js ends up not on box1, but on box2. That box is still running your older version - the user gets their assets loaded for the older application revision, and gets a broken experience.]]></summary></entry><entry><title type="html">On the way to step functions: geneva_drive</title><link href="https://blog.julik.nl/2026/02/on-the-way-to-step-functions-part-4" rel="alternate" type="text/html" title="On the way to step functions: geneva_drive" /><published>2026-02-07T00:00:00+00:00</published><updated>2026-02-07T00:00:00+00:00</updated><id>https://blog.julik.nl/2026/02/on-the-way-to-step-functions-part-4</id><content type="html" xml:base="https://blog.julik.nl/2026/02/on-the-way-to-step-functions-part-4"><![CDATA[<p>This is the concluding article in the series <em>On the Way to Step Functions</em> - you will find the other articles linked below:</p>

<ul>
  <li><a href="/2026/01/on-the-way-to-step-functions-part-1">On the way to step functions: Part 1</a></li>
  <li><a href="/2026/01/on-the-way-to-step-functions-part-2">On the way to step functions: Part 2</a></li>
  <li><a href="/2026/01/on-the-way-to-step-functions-part-3">On the way to step functions: Part 3</a></li>
</ul>

<p>We have established a few important points so far.</p>

<ul>
  <li>The spirit, the <em>desiderata</em> of durable execution comes from the desire to have marshalable stacks - which are unachievable.</li>
  <li>Second-best option are workflows, which are actually DAGs under the hood.</li>
  <li>The DAG definition and the code inside the nodes are two separate worlds with different semantics.</li>
  <li>Systems that pretend they are one body of execution will make you hurt.</li>
</ul>

<p>So: how do we apply these insights to Rails? Enter <a href="https://github.com/julik/geneva_drive">geneva_drive</a>.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="why-geneva-drive">Why “geneva drive”?</h2>

<p>A <a href="https://en.wikipedia.org/wiki/Geneva_drive">Geneva drive</a> is a gear mechanism that converts continuous rotary motion into intermittent rotary motion. It is the mechanism that makes film projectors advance one frame at a time - continuous energy in, discrete steps out. Which is exactly what we want from our workflow engine: the relentless progression of time, converted into discrete, controlled steps.</p>

<p>Besides, the clockwork theme is already with us thanks to <a href="https://github.com/fxn/zeitwerk">Xavi and zeitwerk.</a></p>

<h2 id="what-is-the-right-api-then">What is the “right API” then?</h2>

<p>It may seem trite, but it should answer my basic requirement of <a href="/2025/10/what-does-intuitive-even-mean">intuitiveness</a> – it should be <em>exactly like other things we already know.</em></p>

<p>That implies:</p>

<ul>
  <li>Using existing, integrated and known dependencies and subsystems</li>
  <li>Utilizing concepts which are immediately recognizable - and malleable!</li>
  <li>Adding just enough sugar to enable new functionality</li>
</ul>

<p>And there was an API like this already! It was <a href="https://github.com/honeybadger-io/heya">heya</a> from Honeybadger. Observe a Heya campaign:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">OnboardingCampaign</span> <span class="o">&lt;</span> <span class="no">ApplicationCampaign</span>
  <span class="n">step</span> <span class="ss">:first</span><span class="p">,</span> <span class="ss">subject: </span><span class="s2">"First subject"</span>

  <span class="n">step</span> <span class="ss">:second</span><span class="p">,</span> <span class="ss">subject: </span><span class="s2">"Second subject"</span>

  <span class="n">step</span> <span class="ss">:third</span><span class="p">,</span> <span class="ss">subject: </span><span class="s2">"Third subject"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>If you have used Rails long enough, this should look very self-explanatory - and, dare I say, <em>intuitive.</em></p>

<p>And the story could have even ended with Heya, except that</p>

<ul>
  <li>That <code class="language-plaintext highlighter-rouge">step</code> accepts options for <code class="language-plaintext highlighter-rouge">ActionMailer</code>, and nothing else. It does not accept blocks.</li>
  <li>You can’t even have instance methods used by steps</li>
  <li>The campaign is a weird-ish “anemic” class that you can’t call anything much on</li>
  <li>State is packaged in a very opaque manner</li>
  <li>A lot of sugar is geared deliberately towards drip campaigns</li>
</ul>

<p>But the rest was near damn perfect! Thus the idea: let’s take this API, remove <em>everything</em> underneath and <em>everything</em> that is too email-specific. And repurpose it towards generic, discrete workflows. Thus: <code class="language-plaintext highlighter-rouge">geneva_drive</code>.</p>

<h2 id="the-hero-pattern">The hero pattern</h2>

<p>The heart of a Rails application is the domain model - and model entities it consists of. Heya exists in some nebulous realm where there is no stable connection between the models of the application and the campaign - except for arbitrary method calls. But actual workflows always have a key model that they operate on, or <em>in service of.</em> Observe:</p>

<ul>
  <li>A <code class="language-plaintext highlighter-rouge">PaymentWorkflow</code> likely manages states and evolutions of a <code class="language-plaintext highlighter-rouge">Payment</code></li>
  <li>An <code class="language-plaintext highlighter-rouge">OnboardingWorkflow</code> is certainly concerned with some <code class="language-plaintext highlighter-rouge">User</code> or <code class="language-plaintext highlighter-rouge">Account</code></li>
  <li>An <code class="language-plaintext highlighter-rouge">ArchivingWorkflow</code> is likely concerned with a <code class="language-plaintext highlighter-rouge">Project</code> or a <code class="language-plaintext highlighter-rouge">Tenant</code></li>
</ul>

<p>Every workflow will likely need a subject - in geneva_drive, we call this the “hero”. And because workflows are <em>actual DB-persisted Rails models</em> - it is a polymorphic association that ties the workflow to any ActiveRecord model:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">TransferWorkflow</span> <span class="o">&lt;</span> <span class="no">GenevaDrive</span><span class="o">::</span><span class="no">Workflow</span>
  <span class="kp">alias_method</span> <span class="ss">:transfer</span><span class="p">,</span> <span class="ss">:hero</span>

  <span class="n">step</span> <span class="ss">:initiate</span> <span class="k">do</span>
    <span class="c1"># ...</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Why “hero”? Because in any story, there is a protagonist. The workflow is not the protagonist - the workflow is the <em>narrative arc</em> that the protagonist goes through. The transfer, the user, the order - that is the hero. The workflow describes the journey.</p>

<p>This is not merely philosophical navel-gazing. The hero pattern has practical consequences. A workflow is not only uniquely identified by its primary key, but can also be uniquely identified by the tuple <code class="language-plaintext highlighter-rouge">{hero_type, hero_id, workflow_type}</code>. You cannot have two <code class="language-plaintext highlighter-rouge">TransferWorkflow</code> instances for the same <code class="language-plaintext highlighter-rouge">Transfer</code> that are in progress - and that is enforced at the level of database constraints. You <em>can</em> have a <code class="language-plaintext highlighter-rouge">TransferWorkflow</code> and an <code class="language-plaintext highlighter-rouge">AuditWorkflow</code> for the same <code class="language-plaintext highlighter-rouge">Transfer</code>, though.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">TransferWorkflow</span><span class="p">.</span><span class="nf">for_hero</span><span class="p">(</span><span class="n">transfer</span><span class="p">)</span> <span class="c1"># =&gt; finds or initializes the workflow</span>
<span class="n">transfer</span><span class="p">.</span><span class="nf">transfer_workflow</span> <span class="c1"># =&gt; if you've set up the association</span>
</code></pre></div></div>

<p>This gives you something that ActiveJob fundamentally cannot provide: stable identity. A job is a ticket to be processed. A workflow is an entity with state.</p>

<h2 id="the-eigenclass-revelation">The eigenclass revelation</h2>

<p>Here is a <code class="language-plaintext highlighter-rouge">geneva_drive</code> workflow:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">TransferWorkflow</span> <span class="o">&lt;</span> <span class="no">GenevaDrive</span><span class="o">::</span><span class="no">Workflow</span>
  <span class="kp">alias_method</span> <span class="ss">:transfer</span><span class="p">,</span> <span class="ss">:hero</span>

  <span class="n">step</span> <span class="ss">:initiate</span><span class="p">,</span> <span class="ss">on_exception: :reattempt!</span> <span class="k">do</span>
    <span class="n">result</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="nf">transfer</span><span class="p">(</span>
      <span class="ss">amount: </span><span class="n">transfer</span><span class="p">.</span><span class="nf">amount</span><span class="p">,</span>
      <span class="ss">idempotency_key: </span><span class="no">Digest</span><span class="o">::</span><span class="no">SHA1</span><span class="p">.</span><span class="nf">hexdigest</span><span class="p">(</span><span class="n">transfer</span><span class="p">.</span><span class="nf">to_param</span><span class="p">)</span>
    <span class="p">)</span>
    <span class="n">transfer</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">external_id: </span><span class="n">result</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span> <span class="ss">state: </span><span class="s2">"in_progress"</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="n">step</span> <span class="ss">:poll</span><span class="p">,</span> <span class="ss">on_exception: :reattempt!</span> <span class="k">do</span>
    <span class="n">status</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="nf">get_transfer</span><span class="p">(</span><span class="n">transfer</span><span class="p">.</span><span class="nf">external_id</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">status</span><span class="p">.</span><span class="nf">ok?</span>
      <span class="n">transfer</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">state: </span><span class="s2">"done"</span><span class="p">)</span>
    <span class="k">elsif</span> <span class="n">status</span><span class="p">.</span><span class="nf">in_progress?</span>
      <span class="n">reattempt!</span> <span class="ss">wait: </span><span class="mi">5</span><span class="p">.</span><span class="nf">seconds</span>
    <span class="k">elsif</span> <span class="n">status</span><span class="p">.</span><span class="nf">rejected?</span>
      <span class="n">transfer</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">state: </span><span class="s2">"failed"</span><span class="p">)</span>
    <span class="k">else</span>
      <span class="n">pause!</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Look at this carefully. Where does the DAG definition live? Where does the imperative code live? If you are used to ActiveRecord’s meta-programming, the answer will be obvious.</p>

<p>The <code class="language-plaintext highlighter-rouge">step :initiate</code> call happens <em>at class definition time</em>. When Ruby parses this file, it evaluates the class body, which includes the <code class="language-plaintext highlighter-rouge">step</code> method calls. These calls register steps in a class-level data structure. This is the <em>eigenclass context</em> - the singleton class of <code class="language-plaintext highlighter-rouge">TransferWorkflow</code>, where class methods and class-level state live.</p>

<p>The block passed to <code class="language-plaintext highlighter-rouge">step</code> - the <code class="language-plaintext highlighter-rouge">do ... end</code> part - is not evaluated at class definition time. It is stored, to be executed later in the context of a <em>workflow instance</em>. When we call <code class="language-plaintext highlighter-rouge">workflow.perform_next_step!</code>, the block runs with <code class="language-plaintext highlighter-rouge">self</code> being the workflow instance, with access to <code class="language-plaintext highlighter-rouge">hero</code>, to <code class="language-plaintext highlighter-rouge">pause!</code>, to <code class="language-plaintext highlighter-rouge">reattempt!</code>.</p>

<p>This is not an accident. This is the perfect mapping of the two-worlds problem onto Ruby’s object model - and moreover, to something we are used to in Rails.</p>

<p><strong>The eigenclass (class body) is your DAG.</strong> It is declarative. It runs once, at load time. It defines structure: what steps exist, what order they come in, what their options are. Like a Nuke script defining nodes and connections. Like Terraform’s HCL defining resources and their dependencies.</p>

<p><strong>Instance methods (step blocks) are your nodes.</strong> They are imperative. They run at execution time, potentially many times, on different machines, minutes or days apart. They do stuff: call APIs, update records, send emails. Like the C++ code inside a Nuke node. Like the Go code inside a Terraform provider.</p>

<p>Ruby’s object model gives us this separation for free. We do not need a special DSL. We do not need to invent new abstractions. We just use the language the way it was designed to be used. The class body runs once and defines structure. Instance methods run later and do work.</p>

<h2 id="the-activerecord-advantage">The ActiveRecord advantage</h2>

<p>Geneva_drive workflows are ActiveRecord models. Not “backed by ActiveRecord” or “persisted to ActiveRecord” - they <em>are</em> ActiveRecord models, using single-table inheritance:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">GenevaDrive::Workflow</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="c1"># ...</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">TransferWorkflow</span> <span class="o">&lt;</span> <span class="no">GenevaDrive</span><span class="o">::</span><span class="no">Workflow</span>
  <span class="c1"># ...</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This has enormous practical benefits.</p>

<p><strong>Queryability.</strong> You can do <code class="language-plaintext highlighter-rouge">TransferWorkflow.performing.where(created_at: 1.hour.ago..)</code> and find workflows that have been stuck for an hour. You can do <code class="language-plaintext highlighter-rouge">User.joins(:onboarding_workflow).where(workflows: {state: :finished})</code> to find users who completed onboarding. This is not possible with ActiveJob, where jobs are opaque blobs in a queue.</p>

<p><strong>Associations.</strong> Your <code class="language-plaintext highlighter-rouge">User</code> model can <code class="language-plaintext highlighter-rouge">has_one :onboarding_workflow, as: :hero</code>. Your workflow can <code class="language-plaintext highlighter-rouge">belongs_to :hero, polymorphic: true</code>. Rails associations just work, because workflows are just models.</p>

<p><strong>Transactions.</strong> When a step runs, it can participate in the same transaction as your business logic. No distributed transactions, no eventual consistency headaches. Just normal Rails database operations.</p>

<p><strong>No external dependencies.</strong> Geneva_drive needs nothing beyond your existing Rails stack. No Redis (unless you are already using it for ActiveJob), no Kafka, no gRPC, no Zookeeper. The database is your single source of truth. This is not a limitation - this is a feature. Every external dependency is a failure mode, is an extra system to learn, to monitor, to update. It is <em>more stuff.</em></p>

<h2 id="waiting-is-easy">Waiting is easy</h2>

<p>Want to have a “burndown” process for wallet reclaim that takes 6 months? That’s very easy with geneva_drive. Since steps run asynchronously and are scheduled via your background jobs, arbitrary waits are possible. Moreover: those waits happen at the level of the DAG, not in your step code - so their clock semantics can be cleanly defined:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">WalletReclaimWorkflow</span> <span class="o">&lt;</span> <span class="no">GenevaDrive</span><span class="o">::</span><span class="no">Workflow</span>
  <span class="n">step</span> <span class="ss">:send_warning</span>
  <span class="mi">3</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span> <span class="c1"># Send 3 warnings, spaced 30 days</span>
    <span class="n">step</span><span class="p">(</span><span class="ss">wait: </span><span class="mi">30</span><span class="p">.</span><span class="nf">days</span><span class="p">)</span> <span class="p">{</span> <span class="n">send_warning</span> <span class="p">}</span>
  <span class="k">end</span>
  <span class="n">step</span><span class="p">(</span><span class="ss">wait: </span><span class="mi">24</span><span class="p">.</span><span class="nf">hours</span><span class="p">)</span> <span class="p">{</span> <span class="n">send_warning</span> <span class="p">}</span>
  <span class="n">step</span> <span class="ss">:perform_wallet_reclaim</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Because we don’t try to pretend these waits are <code class="language-plaintext highlighter-rouge">sleep()</code> calls they fit naturally into the calling convention.</p>

<h2 id="state-machines-within-state-machines">State machines within state machines</h2>

<p>A workflow has states: <code class="language-plaintext highlighter-rouge">ready</code>, <code class="language-plaintext highlighter-rouge">performing</code>, <code class="language-plaintext highlighter-rouge">finished</code>, <code class="language-plaintext highlighter-rouge">canceled</code>, <code class="language-plaintext highlighter-rouge">paused</code>. These are enforced by a state machine that validates transitions. You cannot go from <code class="language-plaintext highlighter-rouge">finished</code> to <code class="language-plaintext highlighter-rouge">performing</code>. You cannot call <code class="language-plaintext highlighter-rouge">cancel!</code> on a workflow that is already <code class="language-plaintext highlighter-rouge">canceled</code>.</p>

<p>But steps also have states. A <code class="language-plaintext highlighter-rouge">StepExecution</code> record - which represents a single attempt to run a step - can be <code class="language-plaintext highlighter-rouge">scheduled</code>, <code class="language-plaintext highlighter-rouge">in_progress</code>, <code class="language-plaintext highlighter-rouge">completed</code>, or <code class="language-plaintext highlighter-rouge">failed</code>. When you call <code class="language-plaintext highlighter-rouge">reattempt! wait: 5.minutes</code>, a new <code class="language-plaintext highlighter-rouge">StepExecution</code> is created with state <code class="language-plaintext highlighter-rouge">scheduled</code>, and an ActiveJob is enqueued to run it in 5 minutes.</p>

<p>This is important: retrying a step creates a <em>new</em> execution record. It does not mutate the existing one. This means you have an audit trail of every attempt. You can see that step <code class="language-plaintext highlighter-rouge">:poll</code> was attempted 47 times over 3 hours before finally succeeding. You can query for workflows whose current step has failed more than 5 times.</p>

<p>The step execution model also solves idempotency for free. When the <code class="language-plaintext highlighter-rouge">PerformStepJob</code> runs, it locks the step execution record and checks its state. If the state is not <code class="language-plaintext highlighter-rouge">scheduled</code>, the job exits early. This prevents double-execution if the job gets enqueued twice, or if the worker crashes and the job gets retried.</p>

<h2 id="flow-control-as-a-first-class-concept">Flow control as a first-class concept</h2>

<p>Inside a step block, you have access to flow control methods:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">step</span> <span class="ss">:check_status</span> <span class="k">do</span>
  <span class="n">status</span> <span class="o">=</span> <span class="n">api</span><span class="p">.</span><span class="nf">get_status</span><span class="p">(</span><span class="n">transfer</span><span class="p">.</span><span class="nf">external_id</span><span class="p">)</span>
  <span class="k">case</span> <span class="n">status</span>
  <span class="k">when</span> <span class="ss">:pending</span> <span class="k">then</span> <span class="n">reattempt!</span> <span class="ss">wait: </span><span class="mi">30</span><span class="p">.</span><span class="nf">seconds</span>
  <span class="k">when</span> <span class="ss">:approved</span> <span class="k">then</span> <span class="kp">nil</span> <span class="c1"># proceed to next step</span>
  <span class="k">when</span> <span class="ss">:rejected</span> <span class="k">then</span> <span class="n">cancel!</span>
  <span class="k">when</span> <span class="ss">:suspicious</span> <span class="k">then</span> <span class="n">pause!</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">reattempt! wait: duration</code> - retry this step after a delay</li>
  <li><code class="language-plaintext highlighter-rouge">skip!</code> - skip this step and proceed to the next</li>
  <li><code class="language-plaintext highlighter-rouge">pause!</code> - pause the workflow for manual intervention</li>
  <li><code class="language-plaintext highlighter-rouge">cancel!</code> - abort the workflow entirely</li>
  <li><code class="language-plaintext highlighter-rouge">finish!</code> - complete the workflow early, skipping remaining steps</li>
</ul>

<p>These are not magic. They are just Ruby <code class="language-plaintext highlighter-rouge">throw</code>s with some additional stack information passed up that the executor catches and handles. When you call <code class="language-plaintext highlighter-rouge">reattempt!</code>, the step block throws a <code class="language-plaintext highlighter-rouge">:reattempt</code> - it’s that mundane. The executor catches it, creates a new scheduled step execution, enqueues a job, and returns. No special runtime, no coroutines, no async/await, no fibers, no <code class="language-plaintext highlighter-rouge">IO.select</code> or reactors. Just <code class="language-plaintext highlighter-rouge">throw</code>.</p>

<p>And you don’t need to <code class="language-plaintext highlighter-rouge">return</code> when controlling flow this way - unlike in ActionController, because that <code class="language-plaintext highlighter-rouge">return</code> would just be extra ceremony. If you want to <code class="language-plaintext highlighter-rouge">jmp</code> you should have the latitude to <code class="language-plaintext highlighter-rouge">jmp</code>, no questions asked.</p>

<h2 id="transactional-boundaries">Transactional boundaries</h2>

<p>One of the trickiest parts of workflow engines is getting the transactional boundaries right. You want your business logic to be transactional. But you do not want to hold a database lock while calling an external API that might take 30 seconds.</p>

<p>Geneva_drive is explicit about this. Database transactions are held only during:</p>

<ol>
  <li><strong>Checkout</strong> - locking the workflow and step execution, validating state</li>
  <li><strong>Scheduling</strong> - creating the next step execution record</li>
  <li><strong>Recording outcomes</strong> - updating the step execution state after completion</li>
</ol>

<p>Your step code runs <em>outside</em> of these transactions. When you call <code class="language-plaintext highlighter-rouge">transfer.update!(state: "in_progress")</code> inside a step, that is a separate transaction from the framework’s bookkeeping. This means your API calls do not hold locks. Your bulk database operations do not block the workflow machinery.</p>

<p>If you need your step logic to be transactional with the framework’s state updates, you can wrap it yourself:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">step</span> <span class="ss">:critical_update</span> <span class="k">do</span>
  <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">transaction</span> <span class="k">do</span>
    <span class="n">transfer</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">state: </span><span class="s2">"processed"</span><span class="p">)</span>
    <span class="n">hero</span><span class="p">.</span><span class="nf">save!</span> <span class="c1"># triggers workflow state update</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>But in most cases, you do not need this. The step execution state machine provides the guarantees you need. If your step crashes after updating the transfer but before the framework records completion, the step will be retried - and your step logic should be idempotent enough to handle that.</p>

<h2 id="how-does-scheduling-work">How does scheduling work?</h2>

<p>Scheduling in geneva_drive is a bit tricky, but very simple once you peel away the sugar. Rails apps, historically, do not come with a good scheduler. This has a number of reasons, one of them being that most Rails apps are distributed systems by default, but the traditional (read: “We Have An Experienced Platform Team”) approach was to outsource scheduling to an orchestrator, like Kubernetes, once your number of machines exceeds 1. And otherwise it would be some kind of cron job or systemd.</p>

<p>The situation has changed with recent versions of ActiveJob, though. With the addition of the <code class="language-plaintext highlighter-rouge">set(wait:)</code> option (and the <code class="language-plaintext highlighter-rouge">scheduled_at</code> job param) it became possible to schedule an ActiveJob arbitrarily far into the future. While there were severe limitations to this - like the SQS’s “900 seconds” max delay - or storage constraints, like Redis (for Sidekiq) being “ephemeral” storage - this enabled scheduling patterns that would satisfy the main constraints one usually needs met:</p>

<ul>
  <li>There is at-least-once execution</li>
  <li>A consistent clock source is used (or at least available) for scheduling times</li>
</ul>

<p>geneva_drive uses a very simple setup for this, which accommodates the fact that ActiveJob adapters <em>may</em> be, under certain conditions, somewhat flaky. When you schedule a <code class="language-plaintext highlighter-rouge">step</code> to run, a <code class="language-plaintext highlighter-rouge">StepExecution</code> record gets inserted into your database. That record is <em>guaranteed</em> to have committed before anything gets prepared for actually executing the step. Then, an ActiveJob gets enqueued, with roughly the following shape:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">StepJob</span> <span class="o">&lt;</span> <span class="no">ActiveJob</span><span class="o">::</span><span class="no">Base</span>
  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">step_execution_id</span><span class="p">)</span>
    <span class="c1"># ...</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The job then gets enqueued setting the <code class="language-plaintext highlighter-rouge">wait_until</code> option:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">StepJob</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="ss">wait_until: </span><span class="n">step_execution</span><span class="p">.</span><span class="nf">scheduled_at</span><span class="p">).</span><span class="nf">perform_later</span><span class="p">(</span><span class="n">step_execution</span><span class="p">)</span>
</code></pre></div></div>

<p>This is very small, very simple and robust. Moreover, it allows meaningful recovery! Imagine your entire job queue storage got wiped (Redis down and your Experienced Platform Team™ has not configured proper Redis backups or failover), for example? That’s no big deal. Find all step executions which are pending and re-schedule them:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">GenevaDrive</span><span class="o">::</span><span class="no">StepExecution</span><span class="p">.</span><span class="nf">pending</span><span class="p">.</span><span class="nf">find_each</span> <span class="k">do</span> <span class="o">|</span><span class="n">step_execution</span><span class="o">|</span>
  <span class="no">GenevaDrive</span><span class="o">::</span><span class="no">PerformStepJob</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="ss">wait_until: </span><span class="n">step_execution</span><span class="p">.</span><span class="nf">scheduled_at</span><span class="p">).</span><span class="nf">perform_later</span><span class="p">(</span><span class="n">step_execution</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The ActiveJob is very small for a reason: exactly because ActiveJobs are opaque, not cleanly identifiable and work very differently depending on the adapter, the job is just the “finger that pulls the trigger”. The rest of the operation is backed by your transactional database and models, which are known - and designed - to be robust, with proper consistency and durability guarantees.</p>

<p>Yes, this does result in some latency (in production we do see about 100ms between steps for everything to get committed-then-recalled), but that setup gives you a lot of stability and consistency. And makes the entire workflow recoverable should your ActiveJob queue throw a wobbly!</p>

<h2 id="idempotency-remains-your-responsibility">Idempotency remains your responsibility</h2>

<p>This is worth emphasizing: geneva_drive does not magically make your code idempotent. If your step calls <code class="language-plaintext highlighter-rouge">PaymentGateway.charge(user)</code> and crashes after the charge succeeds but before recording completion, the retry will charge the user again. That is your bug, not the framework’s.</p>

<p>The framework gives you tools to help. The <code class="language-plaintext highlighter-rouge">hero</code> is always available, so you can store idempotency keys on it:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">step</span> <span class="ss">:charge</span> <span class="k">do</span>
  <span class="n">idempotency_key</span> <span class="o">=</span> <span class="no">Digest</span><span class="o">::</span><span class="no">SHA1</span><span class="p">.</span><span class="nf">hexdigest</span><span class="p">(</span><span class="s2">"charge-</span><span class="si">#{</span><span class="n">transfer</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="n">step_execution</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
  <span class="no">PaymentGateway</span><span class="p">.</span><span class="nf">charge</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="ss">idempotency_key: </span><span class="n">idempotency_key</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">step_execution</code> is also available, giving you a unique identifier for this specific attempt. But you have to use these tools. The framework cannot know which of your operations have side effects and which do not.</p>

<p>This is the same responsibility you have with any durable execution system. Temporal does not magically make your activities idempotent either - it just replays them if they were not recorded as complete. The difference is that geneva_drive does not pretend otherwise.</p>

<h2 id="testing-with-time-travel">Testing with time travel</h2>

<p>A workflow that waits 10 days between steps is hard to test. And 10 days is hardly the limit: some workflows are designed to run over <em>months.</em> You do not want your test suite to actually wait 10 days. So a little time-travel to the tune of <code class="language-plaintext highlighter-rouge">ActiveSupport::TestCase</code> is very much needed - and it’s there!</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">TransferWorkflowTest</span> <span class="o">&lt;</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
  <span class="kp">include</span> <span class="no">GenevaDrive</span><span class="o">::</span><span class="no">TestHelpers</span>

  <span class="nb">test</span> <span class="s2">"workflow completes after polling"</span> <span class="k">do</span>
    <span class="n">transfer</span> <span class="o">=</span> <span class="n">transfers</span><span class="p">(</span><span class="ss">:pending</span><span class="p">)</span>
    <span class="n">workflow</span> <span class="o">=</span> <span class="no">TransferWorkflow</span><span class="p">.</span><span class="nf">for_hero</span><span class="p">(</span><span class="n">transfer</span><span class="p">)</span>

    <span class="c1"># This will fast-forward through all the `wait:` durations</span>
    <span class="n">speedrun_workflow</span><span class="p">(</span><span class="n">workflow</span><span class="p">)</span>

    <span class="n">assert</span> <span class="n">workflow</span><span class="p">.</span><span class="nf">finished?</span>
    <span class="n">assert</span> <span class="n">transfer</span><span class="p">.</span><span class="nf">done?</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">speedrun_workflow</code> helper runs the workflow synchronously, using <code class="language-plaintext highlighter-rouge">travel_to</code> to skip past any scheduled delays. Your 10-day workflow completes in milliseconds. This is only possible because the workflow definition is declarative - we can introspect the structure and know exactly when each step is scheduled to run.</p>

<h2 id="what-geneva_drive-does-not-do">What geneva_drive does not do</h2>

<p>Geneva_drive is deliberately constrained in scope. It is a scheduling and state-machine layer, not a distributed transaction coordinator. Here is what it does <em>not</em> provide:</p>

<p><strong>Sagas and rollback.</strong> If step 3 fails and you need to undo steps 1 and 2, that is your responsibility. You can implement compensation logic in your step code, but the framework does not have a declarative rollback mechanism.</p>

<p><strong>Mid-step suspension (yet).</strong> You cannot pause a step halfway through and resume from that point. Steps are atomic units of work. If you need fine-grained suspension, break your work into smaller steps.</p>

<p><strong>Parallel execution (yet).</strong> The current version executes steps sequentially. Fork-and-join is on the roadmap, but not implemented. If you need parallelism today, you can spawn separate workflows and wait for them in a polling step. Or combine geneva_drive with <a href="https://github.com/julik/scatter_gather">scatter_gather</a>.</p>

<p><strong>Admin UI (in the base version).</strong> The open-source version is the workflow engine. The geneva_drive admin UI is already being tested inside of select products - and, trust me, it is neat - but it likely is going to be a paid offering.</p>

<p>These are conscious trade-offs. Every feature adds complexity, and complexity adds bugs. Geneva_drive aims to do one thing well: provide a durable, queryable, ActiveRecord-native workflow engine. It is not trying to be Temporal.</p>

<h2 id="the-bigger-picture">The bigger picture</h2>

<p>If you have read this far, you might be wondering: is all this really necessary? Cannot we just use ActiveJob with some retry logic?</p>

<p>You can. And for many use cases, you should. If your workflow is “send email, done” - you do not need geneva_drive. If your workflow is “process payment, wait for webhook, update status, wait 10 days, send reminder” - you will have a bad time with ActiveJob. In fact, if you already do the above - you are likely to be having a bad time already. The pain is not technical complexity. The pain is <em>opacity</em>. With pure ActiveJob, you cannot answer: “how many payment workflows are stuck in the polling phase?” You cannot answer: “what is the average time between step 2 and step 3?” You cannot answer: “which users started onboarding but never finished?”</p>

<p>These are business questions, and they require your workflows to be first-class entities with queryable state. Moreover, that state is not packaged into the ActiveJob blob, and there is just one workflow - clearly identifiable, queryable, at your fingertips. That is exactly what we do with durable execution. The continuous churn of background job processing gets converted into discrete, observable, queryable workflow states. One frame at a time.</p>

<h2 id="contenders">Contenders</h2>

<p>There are other approaches to this problem in the Rails ecosystem, and they deserve consideration. Each makes different trade-offs, and understanding those trade-offs helps clarify why geneva_drive exists.</p>

<h3 id="ductwork">ductwork</h3>

<p><a href="https://github.com/ductwork/ductwork">Ductwork</a> is a well-designed gem for building data pipelines. Its API is fluent and expressive:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">EnrichUserDataPipeline</span> <span class="o">&lt;</span> <span class="no">Ductwork</span><span class="o">::</span><span class="no">Pipeline</span>
  <span class="n">define</span> <span class="k">do</span> <span class="o">|</span><span class="n">pipeline</span><span class="o">|</span>
    <span class="n">pipeline</span><span class="p">.</span><span class="nf">start</span><span class="p">(</span><span class="no">QueryUsersRequiringEnrichment</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">expand</span><span class="p">(</span><span class="ss">to: </span><span class="no">LoadUserData</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">divide</span><span class="p">(</span><span class="ss">to: </span><span class="p">[</span><span class="no">FetchDataFromSourceA</span><span class="p">,</span> <span class="no">FetchDataFromSourceB</span><span class="p">])</span>
            <span class="p">.</span><span class="nf">combine</span><span class="p">(</span><span class="ss">into: </span><span class="no">CollateUserData</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">chain</span><span class="p">(</span><span class="no">UpdateUserData</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">collapse</span><span class="p">(</span><span class="ss">into: </span><span class="no">ReportSuccess</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The design is solid for its intended purpose: ETL jobs, data enrichment, batch processing. Data flows between steps as return values, and the <code class="language-plaintext highlighter-rouge">expand</code>/<code class="language-plaintext highlighter-rouge">divide</code>/<code class="language-plaintext highlighter-rouge">combine</code>/<code class="language-plaintext highlighter-rouge">collapse</code> primitives handle fan-out and fan-in elegantly.</p>

<p>However, it is optimized for a different problem than business workflows. In ductwork, there is no persistent “subject” of the workflow - data flows through pipes. This is perfect when you are processing a batch of records, but less ideal when you want to say “show me all payment workflows that are stuck” or “what is the state of this user’s onboarding?” The pipeline model does not naturally support querying workflow state by the entity being operated on.</p>

<p>The requirement that return values be JSON-serializable also reveals a different design philosophy. In ductwork, steps communicate through their outputs. In geneva_drive, steps communicate through the hero - the shared, persistent subject that every step can read and modify. Neither approach is wrong; they serve different needs.</p>

<p>I do have <em>severe</em> reservations against the API design here (like using Ruby modules as arguments, or trying to construct a lodash alternative inside Rails) but those are questions of taste, rather.</p>

<h3 id="activejobcontinuation">ActiveJob::Continuation</h3>

<p>Rails 8 introduced <a href="https://github.com/rails/rails/blob/main/activejob/lib/active_job/continuation.rb">ActiveJob::Continuation</a>, which adds step-based execution to ActiveJob:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">PaymentJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
  <span class="kp">include</span> <span class="no">ActiveJob</span><span class="o">::</span><span class="no">Continuation</span>

  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">payment</span><span class="p">)</span>
    <span class="n">step</span> <span class="ss">:authorize</span> <span class="k">do</span>
      <span class="c1"># ...</span>
    <span class="k">end</span>

    <span class="n">step</span> <span class="ss">:capture</span> <span class="k">do</span>
      <span class="c1"># ...</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This is a pragmatic addition to ActiveJob that solves real problems. If you are already using ActiveJob and need basic step tracking, it is a reasonable choice with minimal ceremony.</p>

<p>The design difference from geneva_drive is subtle but significant: the <code class="language-plaintext highlighter-rouge">step</code> calls happen at execution time, inside <code class="language-plaintext highlighter-rouge">perform</code>. Every time the job runs, it re-evaluates which steps exist. This means you cannot introspect the workflow structure at class level - you would need to run the job to discover its steps. For simple linear workflows this is rarely a problem. For complex workflows where you want tooling to understand the structure without executing it, it becomes a limitation. It is exactly <strong>the issue of mixing the DAG and the nodes</strong> inside of one body of code. There is no <code class="language-plaintext highlighter-rouge">steps</code> class method in <code class="language-plaintext highlighter-rouge">ActiveJob::Continuation</code> that you can use to introspect your steps. There <a href="https://github.com/julik/geneva_drive/blob/ffbad401764b206ddc4ad1541eef7a6c12cf80e9/lib/geneva_drive/workflow.rb#L191">absolutely is</a> one in geneva_drive.</p>

<p>ActiveJob::Continuation also inherits ActiveJob’s identity model: jobs are tickets to be processed, not entities with persistent state. You cannot easily query “all payment jobs that completed step 1 but not step 2” because that information lives in the job’s serialized arguments, not in a queryable form.</p>

<h3 id="goodjob-batches">GoodJob batches</h3>

<p><a href="https://github.com/bensheldon/good_job">GoodJob</a> is an excellent ActiveJob backend, and its <a href="https://github.com/bensheldon/good_job#batches">batch</a> feature provides a way to group related jobs:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">batch</span> <span class="o">=</span> <span class="no">GoodJob</span><span class="o">::</span><span class="no">Batch</span><span class="p">.</span><span class="nf">enqueue</span><span class="p">(</span><span class="ss">on_finish: </span><span class="no">BatchCallbackJob</span><span class="p">)</span> <span class="k">do</span>
  <span class="no">User</span><span class="p">.</span><span class="nf">find_each</span> <span class="k">do</span> <span class="o">|</span><span class="n">user</span><span class="o">|</span>
    <span class="no">SyncUserJob</span><span class="p">.</span><span class="nf">perform_later</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Batches solve a real coordination problem: “run these N jobs and do something when they all complete.” This is genuinely useful for fan-out/fan-in patterns.</p>

<p>However, batches are not workflows. A batch is a collection of independent jobs with a completion callback. There is no sequential step progression, no conditional branching based on step results, no “wait 10 days then do the next thing.” Batches answer “did all these jobs finish?” but not “what step is this workflow on?” or “why did this workflow pause?”</p>

<p>You could build workflow-like behavior by chaining batches through callbacks, but at that point you are fighting the abstraction rather than using it. GoodJob batches are excellent at what they do; they are just solving a different problem than durable workflows. Use them for that problem!</p>

<h3 id="temporal">Temporal</h3>

<p><a href="https://temporal.io">Temporal</a> is the heavyweight champion of durable execution. It provides strong guarantees, sophisticated failure handling, and scales to enormous workloads.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">payment_workflow</span><span class="p">(</span><span class="n">payment_id</span><span class="p">)</span>
  <span class="n">authorize_result</span> <span class="o">=</span> <span class="n">workflow</span><span class="p">.</span><span class="nf">execute_activity</span><span class="p">(</span><span class="ss">:authorize_payment</span><span class="p">,</span> <span class="n">payment_id</span><span class="p">)</span>
  <span class="k">if</span> <span class="n">authorize_result</span><span class="p">.</span><span class="nf">success?</span>
    <span class="n">workflow</span><span class="p">.</span><span class="nf">execute_activity</span><span class="p">(</span><span class="ss">:capture_payment</span><span class="p">,</span> <span class="n">payment_id</span><span class="p">)</span>
  <span class="k">end</span>
  <span class="n">workflow</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mi">10</span><span class="p">.</span><span class="nf">days</span><span class="p">)</span>
  <span class="n">workflow</span><span class="p">.</span><span class="nf">execute_activity</span><span class="p">(</span><span class="ss">:finalize_payment</span><span class="p">,</span> <span class="n">payment_id</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>For organizations with dedicated platform teams and complex distributed systems, Temporal is worth serious consideration. Its replay-based execution model is clever: the workflow function runs from the beginning on every resumption, but activities whose results are already recorded get skipped. This lets you write workflows that <em>look</em> imperative while getting durability guarantees.</p>

<p>The trade-offs are operational complexity (you need to run the Temporal server, which needs its own database and potentially Elasticsearch), a steeper learning curve (the distinction between workflows and activities, determinism requirements, and replay semantics take time to internalize), and the fact that workflow state lives outside your Rails database. For teams already running Kubernetes with dedicated SREs, these may be acceptable costs. For a Rails team that wants workflows without introducing new infrastructure, they may not be. There is also a layer of “we pretend to need extreme performance” involving waiting on an Activity using fibers, and downright Ruby-hostile components like gRPC to take into account.</p>

<p>And the most important of all: I find Temporal <em>incredibly</em> over-engineered. If you already have most of Rails at your disposal, using Temporal is like having to install a Panamax supertanker to refuel your Honda Civic.</p>

<p>Geneva_drive is not trying to compete with Temporal on features. It is trying to provide “good enough” durable workflows for Rails applications, without <em>having to deal with all of that stuff.</em></p>

<h2 id="do-take-it-for-a-spin">Do take it for a spin!</h2>

<p>geneva_drive is already in production at <a href="https://cora.computer">Cora</a> and at a couple of other fairly high-intensity applications, with great results. It is the distillation of several years of experience working in fintech and a couple of decades of Rails, and I am convinced it is better than “passable” - it has great future.</p>

<p>The source code is at <a href="https://github.com/julik/geneva_drive">github.com/julik/geneva_drive</a>, licensed under LGPLv3. A commercial version is already available, which includes a fabulous admin UI - I will be posting more about its workings soon - and the removal of the restrictions of LGPL should your company require it. As a teaser, here is how a workflow looks when displayed by the workflow timeline visualiser that you get in the admin:</p>

<p><img src="/assets/geneva-drive-admin/workflow-viz-admin.png" alt="Workflow Visualisation" /></p>

<p>If you are just starting on your product, we may be able to work out a good pricing for you - <em>under the condition</em> that you provide feedback about what is working and what is missing for you to be successful with geneva_drive. I would love you to build something awesome with it.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[This is the concluding article in the series On the Way to Step Functions - you will find the other articles linked below: On the way to step functions: Part 1 On the way to step functions: Part 2 On the way to step functions: Part 3 We have established a few important points so far. The spirit, the desiderata of durable execution comes from the desire to have marshalable stacks - which are unachievable. Second-best option are workflows, which are actually DAGs under the hood. The DAG definition and the code inside the nodes are two separate worlds with different semantics. Systems that pretend they are one body of execution will make you hurt. So: how do we apply these insights to Rails? Enter geneva_drive.]]></summary></entry><entry><title type="html">We have to re-learn to walk alone</title><link href="https://blog.julik.nl/2026/01/go-fast-and-go-alone" rel="alternate" type="text/html" title="We have to re-learn to walk alone" /><published>2026-01-23T00:00:00+00:00</published><updated>2026-01-23T00:00:00+00:00</updated><id>https://blog.julik.nl/2026/01/go-fast-and-go-alone</id><content type="html" xml:base="https://blog.julik.nl/2026/01/go-fast-and-go-alone"><![CDATA[<p>Reading <a href="https://obie.medium.com/what-used-to-take-months-now-takes-days-cc8883cc21e9">the article about Nexus that Obie has posted</a> I got absolutely struck. Yes, it can be said without a shade of doubt that the modern way of building software, the late-2025-way with Opus 4.5 in the picture, is markedly different from the one we operated in for the last decade (or more).</p>

<p>I haven’t been sitting on my hands either. One of my smaller pursuits, now that I am <a href="/hire-me">a proud self-employed raconteur,</a> has been <code class="language-plaintext highlighter-rouge">moneymaker</code> - a piece of kit I wanted for all the business aspects of the said raconteurship. Think:</p>

<ul>
  <li>basic accounting</li>
  <li>bank statements and balances</li>
  <li>invoicing</li>
  <li>time tracking</li>
  <li>…all of that - across several projects, clients and managing entities</li>
</ul>

<p>And, out of principle, I’ve decided to take my chances and release the reins - let the models “drive” the application much more than I did in the past 6 months or so. Because it is important to learn, first and foremost - how to <em>care less about code.</em></p>

<p>And my experience has not been dissimilar from what Obie describes, with a few important differences. But what struck me much stronger is <em>how other humans fit into this.</em></p>

<p>Let me explain. If you squint, you will notice one element missing from Obie’s story: <a href="https://www.philosopheasy.com/p/sartre-the-terrifying-reason-hell">other team members.</a></p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>In the world of product development that we all got used to from start-ups (and - especially so - scale-ups) of the late 2010s we have retained a number of maxims about product development - and the software engineering craft - that looked immutable.</p>

<ul>
  <li>The team has to be… there has to be a team, basically.</li>
  <li>It has to be inclusive and play to everyone’s strengths</li>
  <li>Most decisions happen by consensus</li>
  <li>If you want a decision touching someone’s professional “turf”, you must route it through that individual (UX, QA, compliance, DBA, infrastructure/platform)</li>
</ul>

<p>It looked like building a web product was not going to be the creative pursuit it has been from the start of the early Web, but some refined exercise in near-Soviet bureaucracy combined with an incredibly touchy-feely eggshell walks, at every step.</p>

<p>But LLM-assisted development shifts the gears on this, and I am happy it is happening. Because the way it used to work was not sustainable, not tenable - and highly depressing for anyone having the audacity to <em>actually care about what they build.</em></p>

<p>Here are my thoughts about how to lean into the new era - note that this is my personal opinion, and by no means do you <em>have</em> to do any of that. I do bet though, with a degree of certainty, that top-notch joints who will be able to ride the new process will do either all of those things, or most.</p>

<h2 id="shift-consensus-left">Shift consensus left</h2>

<p>While we did gain a lot in inclusivity and kindness in the times of ZIRP, we also grew a big dependence on <em>consensus.</em> Will Jane like this pre-commit hook? Will Bart want to review the button alignment before we ship? Will Leontien from legal not want to first write a policy for this feature?</p>

<p>LLM-assisted development presents an incredible challenge for applying consensus at any given level: practically anywhere a requirement for consensus will mean that <em>both the operator and the model will have to stop</em> to obtain it. The key reason why Obie is describing his incredible velocity with Nexus is not only that he knew, what he wanted - in detail! - but also that the approaches were not questioned!</p>

<p>Imagine the same happening in a high-consensus setting, where the operator is not solo and not in a leadership role - and not autonomous. Imagine Obie and I work together within an engineering team - just pretend, ok? I would have questioned the use of a triplet store and RDF quite vehemently, because this - in my book - is “interesting tech that makes the author feel clever for using it, but brings little value”. Imagine this team was high-consensus, the engineering manager was “more of a people person” and absent, and clear delegation was not defined. I can tell what would have happened: that system would not have been delivered that quick.</p>

<p>And those consensus junctions come up all the time. And the more people are involved - at every level - the more of those points there will be. The more politicised the org is where consensus must be obtained, the more cautious people will be with OK’ing others’ decisions. Is it “safe” promotion-wise to let that peer “ship whatever they want?” Aren’t they too full of themselves? Can you really “just do things”?</p>

<p>If we want to benefit from the LLM product building approach, we must</p>

<ul>
  <li>…reduce the number of consensus junctions</li>
  <li>…shift them to earlier and earlier phases, with highest level stakeholders participating - and making decisions at broader levels</li>
</ul>

<p>Every approval, every discussion, every conflict and competition causes this workflow to get closer and closer to the “previous” model of software development. Yes, the pinnacle of the “new world” is that with every piece of software there is exactly one developer/designer who has 100% control over what gets built, ideally - a solo founder. But this is not attainable nor scalable, and if some products are gigantic already - what will be the expected growth of them with the LLM tools?</p>

<p>So: find where consensus junctions are present, and at least <em>try</em> to shift them left, and delegate the rest. And yes, it does mean “less consensus, more agency”. Are there 3 people responsible for making a decision? Split it into parts: a smaller one requiring consensus of, say, 2 people (but higher up the chart) and the part that can be given to the operator. You do reviews with 2 colleagues? Try doing reviews with 1, if you know that reviews are often not for fault-finding or correcting things - but for bikeshedding.</p>

<h2 id="shift-micromanagement-right">Shift micromanagement right</h2>

<p>As already mentioned, our work largely consists of making a very large number of decisions. Every decision that needs synchronization with others creates a hiccup: in a highly iterative process those decisions can be made very quickly, provided there is as little approval as possible between an LLM proposing something and a human allowing it to continue or not.</p>

<p>And here comes another twist. The reason why it is possible to work so fast this way is that there is much less rote “coding” and much more “assembly”. You don’t code every function by hand - you either <strong>imagine</strong> what the outcome should look like, or you let the model give you options. The key is having a vision of what you want in the end. If that vision is strong and detailed - and you are not being micromanaged on every decision - that vision can become a perfect roadmap for the model to execute.</p>

<p>The issue is, rather, that this requires a confluence of 2 factors.</p>

<ul>
  <li>The operator must either have “A-to-Z” vision or be able (and permitted!) to improvise along the way</li>
  <li>The operator should not have to stop at every edit and ask someone “is that OK?”</li>
</ul>

<p>Instead of the operator getting micromanaged by other people (peers, his engineering lead, stakeholders) - he should be focusing on micromanaging the model and the piece of work being shaped. This requires being explicit and directive - what <a href="https://www.seuros.com/blog/dictatorship-driven-development/">ADDD calls “dictating” rather than asking</a>. You code <em>with</em> the AI, not the other way around.</p>

<ul>
  <li><em>This button is misaligned, align it</em></li>
  <li><em>You have created an indirection of 4 methods which are only used in one spot, combine and inline all of them</em></li>
  <li><em>Examine this change from the point of view of a security consultant, what is broken?</em></li>
</ul>

<p>The faster you want to go when <a href="https://newsletter.pragmaticengineer.com/p/when-ai-writes-almost-all-code-what">code is no longer the bottleneck</a> - the more agency the operator needs, and the less micromanagement should be permeating what is “to the left” of them.</p>

<p>And it is hard. And I mean: <strong>hard.</strong> I’ve been in situations where the engineering leadership was nearly absent from the “hands on” conversations, and I’ve been in situations where the engineering leadership “wants to play with the rest”. And I undoubtedly prefer the latter. It does have a pitfall, though: for a person with a decent vision of what-is-it-we-are-making-here and sufficient authority it can become <em>unbearable</em> to try and translate their vision to another team memeber. They can hammer that vision into an LLM with a stick and some swearing, spending quite a bit of time - but what do they then have that team member for? Is he worth his salary then?</p>

<p>So the incredibly, incredibly heavy challenge of this new way of working is having sufficient trust to delegate. And it also requires swallowing your pride sometimes, and realizing that the other person <em>will</em> drive the process in a slightly different way than you wanted. You can <em>direct</em> but do it at the right level! The micromanagement of the execution should be with the operator <em>talking</em> to the model and making edits.</p>

<h2 id="but-how-do-we-even-maintain-quality">But how do we even maintain quality?</h2>

<p>A sophisticated stochastic emitter - which is what those models are - is just that, a stochastic emitter. It will produce unexpected results, it will produce garbage - and it is not gifted with telepathy. However, unlike the subjective, touchy-feely humans it can be imperatively instructed and harnessed into conformance - which is exactly how quality can be maintained.</p>

<p>I am not talking about the same level of quality we got used to in the well-cushioned ZIRP years, where choosing between a service object and a command could take 4 people a better part of a few days. This is no longer feasible, simply because the amount of output will increase both factually (very simply: LLMs spit out more code than a human to achieve the same objective) and <em>optically</em> - if the velocity of development is now demonstrably higher, make no mistake: the moving forces of the modern enterprise will also <em>demand</em> more output. For the same compensation, within the same - or drastically shorter - deadlines.</p>

<p>But quite a bit of quality <em>can</em> and <em>should</em> be maintained, even in this wild world of ours. The premise is simple: we need blanket tools, which require minimal configuration, allowing the most bang-for-the-buck impact for safeguarding quality.</p>

<p>Just a few examples. <strong>Tests</strong> (and types) ensure that the code does what it is supposed to do. Because we are dealing with a piece of software - and not a human - it is perfectly fine to do <strong>Befehl-driven-development</strong> - a.k.a. committing the cardinal sin of the late 2010-s by telling the executing party to do things <em>exactly as you say.</em> This type of guidance is very blunt, very violent and uncompromising - but it is perfect for providing guardrails. For example:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>I need you to write tests for the TransactionIngester first. Make the tests very high-level. Ensure that loading test/mutations_camt.xml produces at least 20 bank transactions, and validate all of their amounts to be correct and have at least 2 decimal places. Ensure the currency is the same across all of them. Once you have the tests written, stop and let me verify.
</code></pre></div></div>

<p>This sort of thing would be absolutely frowned upon just a year or two ago. <em>How dare you</em> tell which tests have to be written? <em>How dare you</em> specify the exact things to be asserted on, and <em>how dare you</em> specify that the test should be high level?</p>

<p>You know what happened to people giving this kind of direction about 2018? They got dragged to HR, that’s what happened.</p>

<p>In actuality, there are answers to all of those questions. The tests should be high level so that the most of the system gets exercised with the minimum amount of setup having to be done. The tests should do basic assertions because too many assertions which are too specific make tests brittle. The tests should assert the currency is set correctly (and the same) because with that particular standard all of the entries imported from one file must have the same currency. And the assertion should be for <em>the same currency</em> and not for <em>EUR</em>. Why? Because the file may get updated or replaced with another - the assertions have to keep working and they have to make sense.</p>

<p>Telling a junior (or even worse: a medior) developer to “write tests exactly so” would quickly create resentment if the execution is handled by a human, and would take a great amount of skill - also in hiring - to both have it done and not have the person doing the work hang their hat in desperation.</p>

<p>Yet, those super-exacting instructions - and blanket automated QC maxims - are exactly what is needed with LLM-assisted development. Moreover, they become essential because - while you can’t impose a style and a set of tools on a team of humans - you can, and should, impose it on a stochastic sampler. It is not “micromanaging” - it is <em>setting constraints.</em></p>

<p>This is why <a href="https://www.seuros.com/blog/dictatorship-driven-development/">Agentic Dictatorship-Driven Development (ADDD)</a> works: LLMs are pattern matchers, not entropy generators. When you give vague instructions, they pattern-match to the most common solutions in their training data - purple gradients, Sarah Chen testimonials, AWS setups, WordPress installations. The only way to escape these common attractors is to explicitly close those paths with constraints. “No TypeScript package in Bun” prevents Node.js cargo culting. “No gradients” prevents the purple-to-pink default. “No AWS” eliminates CloudFormation suggestions. Anti-requirements are just as important as requirements - they tell the model what <em>not</em> to do, which is often more valuable than telling it what to do.</p>

<p>What are those guardrails? Simple, really - and we know all of them:</p>

<ul>
  <li>Forced precompilation and eager loading. App doesn’t build or doesn’t load? Redo.</li>
  <li>Forced linting and reformatting. Linter not happy? Format to satisfy linter, redo.</li>
  <li>High-level tests with maximum amount of triggered systems. Test not green? Redo.</li>
  <li>Automated verifications of output (validators). Generated an invalid JSON? Redo.</li>
</ul>

<p>While insulting to a developer, for LLMs this is <em>the</em> way to maintain quality. Moreover, those tools should be driven <em>at their default settings</em> for as much as possible, because “introducing a linter” - just like introducing an extra check for “our documentation should build without errors” - should be a no-brainer. It is not going to be there for humans to decipher - it is going to be there for the LLMs to be running them, almost on autopilot. Hundreds of times.</p>

<p>And when I say “default” - I mean it. If adding a guardrail requires a discussion about “which test framework do we pick”, then a discussion about “which set of linter rules do we appply”… the guardrails will not be in place for the moment when the model is let loose on the codebase. And adding (and especially removing!) any of the guardrails will be so high-friction that it will be avoided.</p>

<p>So - the first order of business is adding <strong>very high yield, very imperative, caustic guardrails.</strong> With spikes and turrets on them.</p>

<p>And it is really key that adding them should be cheap. Why? Well, just as it becomes much easier to <em>try stuff</em> with the code, it becomes much easier to <em>try stuff</em> with configuration. Tell the model to setup a linter. To auto-apply it, then evaluate yourself whether you are ok with the results. If you are not - revert.</p>

<p>Remember - the process is a stochastic sampling endeavour. At the very minimum you should draw the boundaries where the hitting particles will be accepted as valid choices, and where they won’t be.</p>

<p><strong>That is also remarkably difficult with people.</strong> It is tough to walk up to a person and say “I see you did X, this is unacceptable because <Y> and we will fix it using Z".</Y></p>

<p>Another part is design. The deterministic and robust API surface, combined with… <em>some implementation</em> underneath. No, really: it can be anything. It can be a WASM blob for all I care - if it is concealed behind a decent API and exhibits appropriate behavior - it’s fine. Go ahead and make a mess - it’s not appropriate for all types of software, and not always, but for many.</p>

<h2 id="what-is-for-a-junior-dev-to-do">What is for a junior dev to do?</h2>

<p>This doesn’t leave juniors in a great place, unfortunately. I won’t necessarily agree with <a href="https://x.com/kieranklaassen">Kieran</a> that “you are not going to have a great time if you like to write code”. Nor do I agree with Sahil that “taste is cope”. The issue is more nuanced: driving LLMs to produce software does require:</p>

<ul>
  <li>Stern judgment</li>
  <li>Readiness to command and coerce, rather than hint and stroke</li>
  <li>A good understanding of the subject matter</li>
</ul>

<p>It is indeed so that embracing LLMs into software creation requires <em>delegation</em> - but not only from the operator to the model. Delegation from the customer (or manager) to the operator is also essential, and you won’t have the necessary basis to delegate without fear if the person you are delegating <em>to</em> is simply… not schooled well.</p>

<p>Moreover, the delegation will falter in situations where the model is producing something sensible, but the instructions are bad. For example, if it is clear that the person is working with a system having specific constraints, but the model misedintifies the constraints. An example: the model may assume that you are writing records one by one and the API you are talking to has rate limits for that - but, in actuality, the rate limits are on a strictly defined <em>amount of write load</em> you are causing. If you don’t know where to look - you won’t find it. To be able to steer the model you have to know what you want, exactly - in fact, your ability to <em>see through</em> and micromanage the model is going to remain a requirement!</p>

<p>In effect, I would say that being a junior in today’s market requires a lot of will on the part of the hiring company to teach them. And the “hands off” teaching where you could let a junior drift for months and then “check in” with them every-now-and-then is to be buried. Working in this new reality requires a lot of intensity and focus, and if you want someone to be up to speed quickly - similar intensity in knowledge transfer, as well as <em>transfer of intent,</em> is required. The results you would be getting will be directly (or even exponentially) proportional to how much effort you are ready to put into upskilling a junior, and whether you are ready to outline the actual requirements.</p>

<h2 id="the-weight-of-seniority">The weight of seniority</h2>

<p>And, if I may, there is going to be more of the imperative component (instead of warm and cozy “alignment”) in the work that we do. With manual coding, the assumption was that you “just have to” wait for others to “get it”, or to “approve” something, or to “learn”. With LLMs the tolerance for this waiting becomes significantly smaller, and the waiting itself - very very obvious.</p>

<p>There is the trope of a “micromanaging executive” who goes “heck, I don’t want to negotiate with that engineer - I can just build this myself”. LLMs are <em>extreme enablers</em> in that type of behavior. Pushback becomes very risky, and another side-effect that you can get with teams adopting LLMs will be… teams shrinking. If you do not feel good working with someone, and their skills can be replaced by a machine that can be operated like a tool (and the machine is provided at a tenth of the cost): who will you choose to work with?</p>

<p>That said, if you have a certain number of <em>skilled operators</em> who can expand and detail your vision and drive those agentic flows with vigor, you can build something much more valuable than what was imagined possible.</p>

<h2 id="the-new-quality-calculus">The new quality calculus</h2>

<p>I am aware that following these patterns can be disruptive for some teams. And it - undoubtedly - will be destructive, as well. The quality of “the code” going out the door will change - not necessarily drop, but change in nature. The amount of it will increase, and the amount of features will increase too. Because “trying things” is so easy, there will be even less regard for “how do we shut this down if it doesn’t fly?”, “how does it combine with other features?”. There will be collateral damage, there will be bruised egos - and there undoubtedly will be great software engineers who will emerge from this skirmish strongly jaded.</p>

<p>But here’s something that became possible: you <em>can</em> ask the model “how will it combine with these 7 features once layered”. You can’t ask 7 disparate product managers about the same, because they have conflicting incentives, some of them are clueless and 2 are no longer with the company. You <em>can</em> ask the model, and it will hallucinate you an answer which is, invariably, going to contain some nuggets of truth.</p>

<p>The challenge is that we can’t have our cake and eat it too. We can continue debating Rubocop rules - the problem is that our competitor has built a v2 of their product while we were doing that.</p>

<h2 id="what-this-means">What this means</h2>

<p>The shift is not from “high quality” to “low quality” - it’s from “quality through consensus and careful deliberation” to “quality through automated constraints and rapid iteration”. The guardrails I mentioned earlier - linting, tests, validators - these become your quality mechanism. They replace the human review process not because they’re better at catching subtle bugs, but because they can run hundreds of times without complaint, without scheduling meetings, without creating resentment.</p>

<p>Teams will shrink, but they won’t disappear. What you need are fewer people, but each with more agency and more responsibility. The solo founder is the ideal not because collaboration is bad, but because every additional person adds consensus junctions that slow the LLM-assisted workflow to a crawl. The solution isn’t to eliminate teams - it’s to restructure them around clear delegation boundaries and trust that operators will constrain the model appropriately. And those operators deserve maximum agency.</p>

<p>The apparent contradiction between “don’t micromanage humans” and “micromanage the model” resolves when you realize: humans need autonomy and respect. Models need constraints and explicit instructions. The same directive that would make a junior developer feel belittled is exactly what a stochastic emitter requires to produce consistent output. So this isn’t a contradiction at all - it’s recognizing that different systems require different management approaches.</p>

<p>And yes, delegation becomes harder precisely because it becomes more important. When you can build faster, the cost of bad delegation compounds faster too. But the solution isn’t to avoid delegation - it’s to delegate to people who have the judgment to constrain the model effectively, and to invest heavily in teaching that judgment when it’s missing. And to delegate to <em>fewer people.</em></p>

<p>In that new reality, <a href="https://kobewan.substack.com/p/what-i-no-longer-believe-about-design">clarity is more important than alignment</a> - and yes, “clarity” is just a euphemism for “I tell you what to do and how to do it”. But with LLMs, that directive clarity is exactly what enables speed. The question isn’t whether we’ll have more or less clarity - it’s whether we’ll have the courage to be explicit about what we want, and the wisdom to know when to let the operator improvise within those boundaries.</p>

<hr />

<p>And Obie, please, for the love of all that is good in the world - ditch Medium.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Reading the article about Nexus that Obie has posted I got absolutely struck. Yes, it can be said without a shade of doubt that the modern way of building software, the late-2025-way with Opus 4.5 in the picture, is markedly different from the one we operated in for the last decade (or more). I haven’t been sitting on my hands either. One of my smaller pursuits, now that I am a proud self-employed raconteur, has been moneymaker - a piece of kit I wanted for all the business aspects of the said raconteurship. Think: basic accounting bank statements and balances invoicing time tracking …all of that - across several projects, clients and managing entities And, out of principle, I’ve decided to take my chances and release the reins - let the models “drive” the application much more than I did in the past 6 months or so. Because it is important to learn, first and foremost - how to care less about code. And my experience has not been dissimilar from what Obie describes, with a few important differences. But what struck me much stronger is how other humans fit into this. Let me explain. If you squint, you will notice one element missing from Obie’s story: other team members.]]></summary></entry><entry><title type="html">On the way to step functions: the two worlds</title><link href="https://blog.julik.nl/2026/01/on-the-way-to-step-functions-part-3" rel="alternate" type="text/html" title="On the way to step functions: the two worlds" /><published>2026-01-20T00:00:00+00:00</published><updated>2026-01-20T00:00:00+00:00</updated><id>https://blog.julik.nl/2026/01/on-the-way-to-step-functions-part-3</id><content type="html" xml:base="https://blog.julik.nl/2026/01/on-the-way-to-step-functions-part-3"><![CDATA[<p>This is the next article in the series <em>On the Way to Step Functions</em> - you will find the other articles linked below:</p>

<ul>
  <li><a href="/2026/01/on-the-way-to-step-functions-part-1">On the way to step functions: Part 1</a></li>
  <li><a href="/2026/01/on-the-way-to-step-functions-part-2">On the way to step functions: Part 2</a></li>
  <li><a href="/2026/01/on-the-way-to-step-functions-part-3">On the way to step functions: Part 3</a></li>
  <li><a href="/2026/02/on-the-way-to-step-functions-part-4">On the way to step functions: Part 4</a></li>
</ul>

<p>We have highlighted that DAGs are actually the underpinning of any workflow/durable computation engine (barring serializable continuations). I would like to highlight another aspect of that reality: that there are two worlds.</p>

<p>TL;DR - if you are a Rails user and that topic interests you, skip away to <a href="https://github.com/julik/geneva_drive">geneva_drive</a> which is pretty much how I feel we “should do it” in Rails-land.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="the-magic-wall">The magic wall</h2>

<p>The DAGs we use in professional graphics or audio apps have a very interesting property: the workspace consists of two worlds, and these two worlds are segregated by very strict - and limited - interfaces. For example, here is the UI of Nuke again:</p>

<p><img src="/assets/checkerboard-and-blur.png" alt="Nuke Node Graph Screenshot" /></p>

<p>The nodes that we see in the graph form part of what is called <em>a Nuke script.</em> A working Nuke file is actually a script in a near-forgotten scripting language called <a href="https://en.wikipedia.org/wiki/Tcl">Tcl</a>. Working files for both Shake and Nuke are, historically, scripts - because both applications were designed to work without a GUI at all. They were built as headless render engines for files produced by other applications (or written from scratch).</p>

<p>A Nuke script has the following shape:</p>

<div class="language-tcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code>set cut_paste_input <span class="p">[</span>stack 0<span class="p">]</span>
version 6.3 v4
CheckerBoard2 <span class="p">{</span>
 inputs 0
 name CheckerBoard1
<span class="p">}</span>
Blur <span class="p">{</span>
 size 
 name Blur1
 selected true
 xpos -212
 ypos -24
<span class="p">}</span>
</code></pre></div></div>

<p>We have a <code class="language-plaintext highlighter-rouge">CheckerBoard2</code> node with no inputs. Then the output of the checkerboard is fed into a <code class="language-plaintext highlighter-rouge">Blur</code> (it being right below the checkerboard node means that it accepts the output from the checkerboard - the order of nodes is used for implicit stacking). The <code class="language-plaintext highlighter-rouge">size</code> parameter of the <code class="language-plaintext highlighter-rouge">Blur</code> is also animated, which is done using the `` expression braces.</p>

<p>But there is something else too! The <code class="language-plaintext highlighter-rouge">CheckerBoard2</code> actually instantiates an object in native code, which is compiled from a source file called <code class="language-plaintext highlighter-rouge">CheckerBoard2.cpp</code>. The code that defines the workflow is one entity, while the code that runs inside the nodes is a completely different entity.</p>

<p>And they <em>never</em> intertwine except via the exposed properties from the DAG. The Nuke engine has various handles exposed from <code class="language-plaintext highlighter-rouge">CheckerBoard2</code> for caching, for rendering a pixel or rendering a scanline from it, for getting the image bounding box of the node and for setting its initial parameters. But note that the script does not contain the definition of the <code class="language-plaintext highlighter-rouge">CheckerBoard2</code> module itself!</p>

<p>Moreover: there is absolutely no way for the script to <em>call things</em> on the module or control it imperatively. The workflow engine (the Nuke engine) establishes bindings between the values and expressions in the script, and passes the values in one direction only into every particular node.</p>

<p>When doing the render, Nuke passes the buffer into which the node shall output its rendered pixels as an argument (it is a C++ reference variable for every image row, but that’s an implementation detail) - it <em>collects</em> the result. The result may then be cached somewhere, or the engine may elect to use that cached result and not call the module at all. Or the node may be disconnected from any further outputs - and will then be skipped from initialization.</p>

<p>But the most important trait of this system is that it is effectively 2 separate environments: the declarative code of the workflow and the imperative code of every node, which runs parameterized with the node’s inputs and properties.</p>

<p>The node’s code must also be idempotent.</p>

<p>And these worlds <em>never meet.</em></p>

<h2 id="congratulations-now-your-functions-have-3-colors">Congratulations, now your functions have 3 colors</h2>

<p>I do not find it strange that most workflow systems attempt to fuse these two worlds at any cost. For example, <a href="https://github.com/rails/rails/blob/main/activejob/lib/active_job/continuation.rb">ActiveJob Continuation</a> does its best to stuff all of the step definitions inside of the <code class="language-plaintext highlighter-rouge">perform</code> method of the job class. Temporal is trying desperately to make the entire workflow controllable from a single imperative workflow, under the pretense that it can somehow work - and it does, like any highly leaky abstraction.</p>

<p>You just have to use gRPC, a special flavour of <code class="language-plaintext highlighter-rouge">sleep</code>, a special flavour of… anything, really - but I digress.</p>

<p>Similar for the Vercel workflows and <code class="language-plaintext highlighter-rouge">use workflow</code>. What happens if you do not have a <code class="language-plaintext highlighter-rouge">use workflow</code> (and a JS transpiler - you seem to need a JS transpiler for every teensy little thing these days)? Well, every <code class="language-plaintext highlighter-rouge">async</code> invocation may crash and you will have to restart from scratch. There are implicit checkpoints, but where will they be? What are the guarantees that you won’t rerun what has already run, and is there a guarantee that what gets <code class="language-plaintext highlighter-rouge">await</code>-ed on will be idempotent and produce the same output on every invocation?</p>

<p>Nothing. No - really - while it is very tempting to <em>pretend</em> that you can dispense with that separation without having an entire virtual machine for it - it will not become more robust, or easy to understand, or particularly more “user friendly” because that blending gives your code blocks <em>color,</em> and not in a good way. Remember the epic <a href="https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/">What Color Is Your Function</a> essay? Merely using <code class="language-plaintext highlighter-rouge">async</code> has your program run with 2 colors:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// paymentResult and everything around is mostly "red", what happens inside</span>
<span class="c1">// performPayment is "blue".</span>
<span class="kd">const</span> <span class="nx">paymentResult</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">performPayment</span><span class="p">(</span><span class="nx">paymentRequest</span><span class="p">);</span> 
</code></pre></div></div>

<p>But blending your DAG and your imperative node code now has your code in 3 colors, with wildly different traits. The two we already know:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="dl">"</span><span class="s2">use workflow</span><span class="dl">"</span> <span class="c1">// This turns the entire "outer" code section "green"</span>
<span class="kd">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="nf">generateToken</span><span class="p">();</span> <span class="c1">// This chunk is "red" - maybe</span>
<span class="kd">const</span> <span class="nx">paymentResult</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">performPayment</span><span class="p">(</span><span class="nx">paymentRequest</span><span class="p">,</span> <span class="nx">token</span><span class="p">);</span> <span class="c1">// This is "blue"</span>
</code></pre></div></div>

<p>…are likely going to be transformed by the “workflow compiler” to have checkpoints where it can see you are calling an async function:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="dl">"</span><span class="s2">use workflow</span><span class="dl">"</span> <span class="c1">// This turns the entire "outer" code section "green"</span>
<span class="kd">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="nf">generateToken</span><span class="p">();</span> <span class="c1">// This chunk is "red" - maybe</span>
<span class="nx">__magicalWorkflowRuntime</span><span class="p">.</span><span class="nf">stackPush</span><span class="p">(</span><span class="dl">"</span><span class="s2">token</span><span class="dl">"</span><span class="p">,</span> <span class="nx">token</span><span class="p">)</span>
<span class="nx">__magicalWorkflowRuntime</span><span class="p">.</span><span class="nf">checkpoint</span><span class="p">(</span><span class="nx">__LINENO__</span><span class="p">)</span>
<span class="kd">const</span> <span class="nx">fut</span> <span class="o">=</span> <span class="nx">__magicalWorkflowRuntime</span><span class="p">.</span><span class="nf">futurePromise</span><span class="p">(</span><span class="nx">__LINENO__</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nf">performPayment</span><span class="p">(</span><span class="nx">paymentRequest</span><span class="p">,</span> <span class="nx">token</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">__magicalWorkflowRuntime</span><span class="p">.</span><span class="nf">stackPush</span><span class="p">(</span><span class="dl">"</span><span class="s2">paymentResult</span><span class="dl">"</span><span class="p">,</span> <span class="nx">fut</span><span class="p">);</span>
<span class="nx">__magicalWorkflowRuntime</span><span class="p">.</span><span class="nf">checkpoint</span><span class="p">(</span><span class="nx">__LINENO__</span><span class="p">)</span>
</code></pre></div></div>

<p>You <em>don’t know</em> how the “green” chunk of code will suspend or resume. You don’t know what the idempotency guarantees are in it. If you call the following code in your “green” section, what will happen?</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="dl">"</span><span class="s2">use workflow</span><span class="dl">"</span> <span class="c1">// This turns the entire "outer" code section "green" (replayed + suspendable)</span>
<span class="kd">const</span> <span class="nx">started</span> <span class="o">=</span> <span class="nx">performance</span><span class="p">.</span><span class="nf">now</span><span class="p">();</span> <span class="c1">// "red": time is non-deterministic across suspend/resume/replay</span>
<span class="kd">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="nf">generateToken</span><span class="p">();</span> <span class="c1">// "red": randomness / one-time values (non-deterministic)</span>
<span class="kd">const</span> <span class="nx">paymentResult</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">performPayment</span><span class="p">(</span><span class="nx">paymentRequest</span><span class="p">,</span> <span class="nx">token</span><span class="p">);</span> <span class="c1">// "red": external I/O (retried / resumed elsewhere)</span>
<span class="kd">const</span> <span class="nx">balanceAfter</span> <span class="o">=</span> <span class="nf">extractBalance</span><span class="p">(</span><span class="nx">paymentResult</span><span class="p">);</span> <span class="c1">// "blue": pure/deterministic sync computation</span>
<span class="kd">const</span> <span class="nx">timeTaken</span> <span class="o">=</span> <span class="nx">performance</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span> <span class="o">-</span> <span class="nx">started</span><span class="p">;</span> <span class="c1">// "red": meaningless if the workflow suspends between the two calls</span>
</code></pre></div></div>

<p>We want to measure how long the payment flow took end-to-end, but we can’t! The fact that our “body of code” blends the three different execution flows (sync functions, async functions and suspendable workflow code) means that our second invocation of <code class="language-plaintext highlighter-rouge">performance.now()</code> is going to run on a completely different machine than our first invocation, and it will almost certainly not provide us with a value that makes sense (unless the runtime overrides <code class="language-plaintext highlighter-rouge">performance</code>, which - in the usual Vercel fashion - will likely be done, with disastrous consequences).</p>

<p>But the fact that these two are one and the same body of code lull you into a fake sense of familiarity. It is a lie, and a damn sneaky one.</p>

<h2 id="the-two-worlds">The two worlds</h2>

<p>The definition of the DAG and the code that runs inside its nodes are two completely separate entities. In Terraform, the DAG is defined in HCL and converted into the “state” - a reconstruction of the world with in-memory handles to the nodes. The “providers” - <em>imperative code written in Go</em> - actually <em>execute</em> those nodes. These two worlds only ever combine during the execution of the Terraform tasks.</p>

<p>In Nuke, the “script” (working document) is a description of a DAG which gets loaded into memory and the nodes are, again reconstructed with references to each other. The shared libraries - one library per node implementation - is the <em>imperative code written in C++</em> which actually <em>executes</em> those nodes. These two worlds only ever combine during the rendering (or previewing) your Nuke scene.</p>

<p>Let’s take a look at “use workflow” from Vercel as the latest example of “just keep pretending it’s all one big function, folks!” Sure, it <em>looks</em> lovely: you get to write a single chunk of code, with all the comforting imperative trappings. Sleep here, branch there, loop around—ah, the sweet illusion of continuity! But here’s the punchline: it’s always been a fantasy. All of history’s best practitioners—visual effects applications, Terraform, you name it—acquired their rigor precisely by separating <em>what needs to be done</em> (the declarative world—the DAG) from <em>how it’s actually done</em> (the imperative “do the thing” world). And for good reason: execution is not the same as orchestration!</p>

<p>Insisting on bodying everything up into one pseudo-imperative blob just brings you tricksy bugs and mental contortionism. You start believing that your stops and waits and forks will always “just work” because… JavaScript closure magic will save the day? Please. Ask anyone who tried marshalling a deeply nested workflow stack into the ether and back if they slept well that week.</p>

<p>Embracing DAGs for workflows is the real creative victory here. The moment you admit that the orchestration “map” is a DAG, you unshackle yourself: each step can be resolved, cached, retried, and orchestrated independently. You gain parallelism for free; you can swap nodes and reroute dependencies without rewriting every last “await” or “yield.” It’s not a cop-out or a constraint—it’s <em>liberating</em>. The sooner we stop pretending that everything is “just a function” and instead acknowledge the power of the DAG, the more future-proof, testable, and <em>pleasant</em> our workflow systems become. Vercel, call me when you’re ready to come to Nodes School.</p>

<h2 id="a-word-of-warning-about-yaml">A word of warning about YAML</h2>

<p>It will, of course, be very tempting to say “well, if the DAG should be declarative - why not make it YAML?”</p>

<p>As a matter of fact, HashiCorp have stopped just short of that with Terraform - except they have invented their own language instead. Most systems that come from the computing platforms world insist on using YAML for declarative portions of the code.</p>

<p>Sadly, this is not because “YAML is declarative”. It is because YAML is a least common denominator - the terrible thing everybody could agree on. See, Python folks hate Ruby folks. Ruby folks don’t hate anyone but they don’t like writing Python. Go folks think that both Ruby and Python folks are losers. Rust folks think that all the other folks are quite cute, but whatever they use is a toy anyway. JS folks think that everyone is stupid and that the majority rule by steamrolling will, in the end, make everything JS.</p>

<p>So when the time came to have some kind of declarative language for things like configurations and DAGs, the compromise became YAML. While HCL is a nice departure from YAML it is still very un-ergonomic for someone coming from Ruby or Python - and needlessly so, because while any language can produce a non-deterministic workflow definition, having a language for this you know intimately well will be a better option.</p>

<p>So: if you ever go into durable workflows, make it possible to define the DAG in whatever language that is native and pleasant for the ecosystem(s) you want users in.</p>

<h2 id="to-recap">To recap</h2>

<p>The definition of the DAG and the code that runs inside its nodes are two completely separate entities. In Terraform, the DAG is defined in HCL and converted into the “state” - a reconstruction of the world with in-memory handles to the nodes. The “providers” - <em>imperative code written in Go</em> - actually <em>execute</em> those nodes. These two worlds only ever combine during the execution of the Terraform tasks.</p>

<p>In Nuke, the “script” (working document) is a description of a DAG which gets loaded into memory and the nodes are, again reconstructed with references to each other. The shared libraries - one library per node implementation - is the <em>imperative code written in C++</em> which actually <em>executes</em> those nodes. These two worlds only ever combine during the rendering (or previewing) your Nuke scene.</p>

<p>Most - if not all - “durable execution” systems will contain imperative parts (the steps/nodes) and declarative parts (the DAG describing your workflow). These two parts have wildly different semantics, guarantees and requirements. Systems that pretend they are one and the same - will create much more pain for their users than those which do not.</p>

<p>Stay tuned for Part 4, where we will take a look at how those workflows could work quite nicely in Ruby and why the existing solutions (active-job-iteration, ActiveJob::Continuation, temporal-ruby etc.) are not quite up to snuff.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[This is the next article in the series On the Way to Step Functions - you will find the other articles linked below: On the way to step functions: Part 1 On the way to step functions: Part 2 On the way to step functions: Part 3 On the way to step functions: Part 4 We have highlighted that DAGs are actually the underpinning of any workflow/durable computation engine (barring serializable continuations). I would like to highlight another aspect of that reality: that there are two worlds. TL;DR - if you are a Rails user and that topic interests you, skip away to geneva_drive which is pretty much how I feel we “should do it” in Rails-land.]]></summary></entry><entry><title type="html">On the way to step functions: it is actually a DAG</title><link href="https://blog.julik.nl/2026/01/on-the-way-to-step-functions-part-2" rel="alternate" type="text/html" title="On the way to step functions: it is actually a DAG" /><published>2026-01-17T00:00:00+00:00</published><updated>2026-01-17T00:00:00+00:00</updated><id>https://blog.julik.nl/2026/01/on-the-way-to-step-functions-part-2</id><content type="html" xml:base="https://blog.julik.nl/2026/01/on-the-way-to-step-functions-part-2"><![CDATA[<p>This is the next article in the series <em>On the Way to Step Functions</em> — you can find the first article in the series <a href="/2026/01/on-the-way-to-step-functions-part-1">here.</a> Previously, I have outlined the ambient desire in the field (marshalable stacks) and described why that is largely unachievable. But if imperative invocations can’t bid us consolation, what could?</p>

<p>DAGs, in fact.</p>

<p>If you are impatient (and a Rails user) - just head to the <a href="https://github.com/julik/geneva_drive">geneva_drive</a> repo for the grand reveal.</p>

<p>This post is part of a series:</p>

<ul>
  <li><a href="/2026/01/on-the-way-to-step-functions-part-1">On the way to step functions: Part 1</a></li>
  <li><a href="/2026/01/on-the-way-to-step-functions-part-2">On the way to step functions: Part 2</a></li>
  <li><a href="/2026/01/on-the-way-to-step-functions-part-3">On the way to step functions: Part 3</a></li>
  <li><a href="/2026/02/on-the-way-to-step-functions-part-4">On the way to step functions: Part 4</a></li>
</ul>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="dags-dags-everywhere">DAGs, DAGs everywhere</h2>

<p>I love DAGs (<a href="https://en.wikipedia.org/wiki/Directed_acyclic_graph">directed acyclic graphs</a>). DAGs are everywhere.</p>

<p>Having worked in visual effects for a while, I know first hand that most of the professional apps implement some version of a DAG. <a href="https://www.blackmagicdesign.com/products/davinciresolve">DaVinci Resolve</a> does. <a href="https://www.foundry.com/products/nuke-family/nuke">Nuke</a> does. <a href="https://www.sidefx.com/products/houdini/">Houdini</a> does. Maya, Fusion, Blender, Flame… the list goes on and on.</p>

<p>The interesting bit is that those systems, while seemingly far estranged from payment processing and other “cloud” workloads, are - in essence - <em>deferred computation engines.</em> Since performing the actual processing (rendering, generation etc.) in those systems can be extremely computationally intensive, placing those computations behind nodes makes for a great way to stabilise and cache the results of those computations. Moreover - to a certain degree using DAGs allows the results to be retained, provided that the dependencies of the nodes do not influence the results of the computation. For example, a node that generates a piece of platonic geometry can be a “leaf” node and not have any dependencies at all - it only has parameters. Therefore, it can not only be reconnected into other dependent nodes - it can also be duplicated, and reuse the stored result of the computation in all of the instances thus created.</p>

<p>This is what node graphs tend to look like in practice (example: Blender shader nodes):</p>

<p><img src="/assets/2025-01-27-on-the-way-to-step-functions-part-2/node-graph.png" alt="A representative node graph (Blender shader nodes)." /></p>

<p>Nuke and Autodesk Flame have similar node graph setups, adapted to their respective domains.</p>

<p>Instead of <em>imperatively specifying the execution flow</em> a DAG specifies a <em>tree of dependencies.</em> When Stephen Margheim was picking up work again on <a href="https://github.com/fractaledmind/acidic_job">acidic_job</a> I’ve asked him about DAGs, and that was for a good reason. If we ignore structured rollbacks (sagas), any such step computation is, actually, a DAG. The “workflow” - the brittle, squishy part responsible for orchestration - lives in the DAG, the nodes and the connections between them. The imperative code that “does stuff” lives in the nodes themselves, oblivious to the fact that it is controlled “from the outside”. And the DAG is, by definition, declarative.</p>

<p>But VFX software is by far not the only rendition of DAGs. Just look at Terraform:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">module</span> <span class="s2">"app"</span> <span class="p">{</span>
  <span class="nx">source</span> <span class="o">=</span> <span class="s2">"../../modules/app_engine_flex"</span>

  <span class="nx">project_id</span>      <span class="o">=</span> <span class="nx">module</span><span class="p">.</span><span class="nx">thefirm_project</span><span class="p">.</span><span class="nx">project_id</span>
  <span class="nx">resource_prefix</span> <span class="o">=</span> <span class="nx">module</span><span class="p">.</span><span class="nx">thefirm_project</span><span class="p">.</span><span class="nx">project_name</span>

  <span class="nx">services</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">default</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nx">allow_external_traffic</span> <span class="o">=</span> <span class="kc">true</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="nx">artifacts_project_id</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">artifacts_registry_project_id</span>
  <span class="nx">subnet_name</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">subnet</span><span class="p">.</span><span class="nx">name</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">app</code> here is a <em>Node</em> in a DAG. It has <code class="language-plaintext highlighter-rouge">thefirm_project</code> as well as <code class="language-plaintext highlighter-rouge">var</code> as its dependencies - before they get resolved, the <code class="language-plaintext highlighter-rouge">app</code> node cannot be resolved. When doing a “plan” (or an “apply”), Terraform will build out the dependency graph of the <em>resources</em> that need to be modified or created to satisfy dependencies of their downstream resources, perform a topological sort, and start with resolving <code class="language-plaintext highlighter-rouge">var</code> and <code class="language-plaintext highlighter-rouge">thefirm_project</code> before trying to resolve <code class="language-plaintext highlighter-rouge">app</code>. There are other peculiar tidbits that allow Terraform to function as a DAG. For example:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"random_id"</span> <span class="s2">"db_suffix"</span> <span class="p">{</span>
  <span class="nx">byte_length</span> <span class="o">=</span> <span class="mi">2</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Seemingly, this would be a value which would completely throw off repeated computation for Terraform. Every time we evaluate the DAG, there would be a new random ID generated by this resource, right?</p>

<p>Right. However, Terraform has this thing called <em>state</em>. The Terraform state is effectively a materialized version of checkpoints, for every node in the graph. Once the <code class="language-plaintext highlighter-rouge">db_suffix</code> resource gets resolved by Terraform, its computation result will be saved into the Terraform state, and frozen until the parameters of the resource change. <code class="language-plaintext highlighter-rouge">byte_length</code> changing would lead to Terraform regenerating the random value. So even though the node is supposed to generate a random value, that value then gets cached and reused - making it an idempotent computation.</p>

<p>If there were a resource called <code class="language-plaintext highlighter-rouge">payment_request</code> which would need an idempotency key, it would be to the author of the Terraform provider to ensure the idempotency key either does not bleed into the orchestration / node checkpoint at all (and is thus always random), or that it gets persisted into the checkpoint and remains stable.</p>

<p>In Nuke or Resolve a similar trick is employed. If you have a node which generates random noise or grain, it would be parametrised by a known <em>seed value</em>. That seed value would be one of the node’s parameters, and would be persisted with the DAG. Rerunning the node’s code should produce exactly the same noise output. The node is thus coded to be <em>idempotent.</em> In this instance, idempotency is also an inherent requirement - but this requirement exists on the level of nodes, not on the level of the orchestrating workflow.</p>

<h2 id="turning-our-invocation-into-a-dag">Turning our invocation into a DAG</h2>

<p>Let’s get back to our example:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">authorisation_result</span> <span class="o">=</span> <span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:authorise_payment</span><span class="p">)</span>
<span class="n">funds_check_result</span> <span class="o">=</span> <span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:check_funds</span><span class="p">)</span>
<span class="nb">sleep</span> <span class="k">until</span> <span class="n">authorisation_result</span><span class="p">.</span><span class="nf">received?</span> <span class="o">&amp;&amp;</span> <span class="n">funds_check_result</span><span class="p">.</span><span class="nf">received?</span>
<span class="n">transfer_result</span> <span class="o">=</span> <span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:transfer_funds</span><span class="p">)</span>
<span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:send_email</span><span class="p">)</span> <span class="k">if</span> <span class="n">transfer_result</span><span class="p">.</span><span class="nf">ok?</span>
<span class="nb">sleep</span> <span class="mi">10</span><span class="p">.</span><span class="nf">days</span>
<span class="n">mark_payment_as_final</span>
</code></pre></div></div>

<p>What we have before us is imperative code - or as close as we can get to imperative code. But we can also represent this workflow as a graph of nodes. If we assume that fetching the result of a <code class="language-plaintext highlighter-rouge">run_remotely_and_asynchronously</code> call equates to needing that result as a dependency for the actions taking place down the program, a DAG representation (assuming a DSL for defining DAGs) could look like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">authorisation</span> <span class="o">=</span> <span class="n">dag</span><span class="p">.</span><span class="nf">create_node</span><span class="p">(</span><span class="ss">:authorise_payment</span><span class="p">)</span>
<span class="n">funds_check</span> <span class="o">=</span> <span class="n">dag</span><span class="p">.</span><span class="nf">create_node</span><span class="p">(</span><span class="ss">:check_funds</span><span class="p">)</span>

<span class="n">join</span> <span class="o">=</span> <span class="n">dag</span><span class="p">.</span><span class="nf">create_node</span><span class="p">(</span><span class="ss">:wait_for_all_inputs</span><span class="p">)</span>
<span class="n">join</span><span class="p">.</span><span class="nf">connect_inputs</span><span class="p">(</span><span class="n">authorisation</span><span class="p">,</span> <span class="n">funds_check</span><span class="p">)</span>

<span class="n">transfer_result</span> <span class="o">=</span> <span class="n">dag</span><span class="p">.</span><span class="nf">create_node</span><span class="p">(</span><span class="ss">:transfer_funds</span><span class="p">)</span>
<span class="n">transfer_result</span><span class="p">.</span><span class="nf">connect_inputs</span><span class="p">(</span><span class="n">join</span><span class="p">)</span>

<span class="n">email</span> <span class="o">=</span> <span class="n">dag</span><span class="p">.</span><span class="nf">create_node</span><span class="p">(</span><span class="ss">:send_email</span><span class="p">)</span>
<span class="c1"># We need some form of branching -</span>
<span class="c1"># this could be one of the possible ways</span>
<span class="n">email</span><span class="p">.</span><span class="nf">connect_inputs</span><span class="p">(</span><span class="n">transfer_result</span><span class="p">.</span><span class="nf">ok_output</span><span class="p">)</span> <span class="c1"># There would also be Node#error_output</span>

<span class="n">wait</span> <span class="o">=</span> <span class="n">dag</span><span class="p">.</span><span class="nf">create_sleep</span><span class="p">(</span><span class="mi">10</span><span class="p">.</span><span class="nf">days</span><span class="p">)</span>
<span class="n">wait</span><span class="p">.</span><span class="nf">connect_inputs</span><span class="p">(</span><span class="n">email</span><span class="p">)</span>

<span class="n">finalize</span> <span class="o">=</span> <span class="n">dag</span><span class="p">.</span><span class="nf">create_node</span> <span class="p">{</span> <span class="n">mark_payment_as_final</span> <span class="p">}</span>
<span class="n">finalize</span><span class="p">.</span><span class="nf">connect_inputs</span><span class="p">(</span><span class="n">wait</span><span class="p">)</span>
</code></pre></div></div>

<p>Or, visualised:</p>

<pre><code class="language-mermaid">graph TD
    A[authorise_payment] --&gt; J[wait_for_all_inputs]
    F[check_funds] --&gt; J
    J --&gt; T[transfer_funds]
    T --&gt;|ok_output| E[send_email]
    E --&gt; W[sleep 10.days]
    W --&gt; FIN[mark_payment_as_final]
</code></pre>

<p>We could then call our <code class="language-plaintext highlighter-rouge">finalize.resolve!</code> to run through the nodes in order. Since this is a DAG, you would run some kind of topological sort first, to resolve the dependencies of all the nodes in your workflow. A topo sort would reveal that you need to resolve <code class="language-plaintext highlighter-rouge">authorisation</code> and <code class="language-plaintext highlighter-rouge">funds_check</code> first, and since they do not depend on anything common, they can be run in parallel.</p>

<p>The obvious item we would need to solve in this situation would be the checkpointing. Node-based system such as the one in Nuke implement it in a fairly interesting way. When the nodes get evaluated, there is a <code class="language-plaintext highlighter-rouge">CacheHash</code> object passed through them. Every node is able to do roughly this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cache_hash_for_this_branch</span> <span class="o">&lt;&lt;</span> <span class="nb">self</span><span class="p">.</span><span class="nf">stable_cache_key_for_source_file</span>
<span class="n">cache_hash_for_this_branch</span> <span class="o">&lt;&lt;</span> <span class="nb">self</span><span class="p">.</span><span class="nf">random_seed_used_for_queries</span>
</code></pre></div></div>

<p>The cache key for a <code class="language-plaintext highlighter-rouge">Node</code> would be an amalgamation of the node’s <code class="language-plaintext highlighter-rouge">CacheHash</code> and the <code class="language-plaintext highlighter-rouge">CacheHash</code> values of all the node’s inputs - you could see it as a <a href="https://en.wikipedia.org/wiki/Merkle_tree">Merkle tree</a> of sorts.</p>

<p>Any change in the input configuration (or in the upstream node parameters) would invalidate that cache. It is a bit baroque but quite effective, <em>unless the graph gets reconfigured</em> - remember what I’ve written about the code of the invocation being stable? With a DAG there can be a formal definition to this stability within the scope of a section of the DAG’s edges - a set of nodes with parameters, connected to one another, remain a stable invocation as long as the parameters of the nodes <em>and</em> the inputs do not change.</p>

<h2 id="steps-are-just-a-subset-of-a-dag">Steps are just a subset of a DAG</h2>

<p>If we treat our workflow as a sequence of steps, like this one (we’ll use a trimmed example of our previous workflow):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">authorisation_result</span> <span class="o">=</span> <span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:authorise_payment</span><span class="p">)</span>
<span class="n">funds_check_result</span> <span class="o">=</span> <span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:check_funds</span><span class="p">)</span>
<span class="nb">sleep</span> <span class="k">until</span> <span class="n">authorisation_result</span><span class="p">.</span><span class="nf">received?</span> <span class="o">&amp;&amp;</span> <span class="n">funds_check_result</span><span class="p">.</span><span class="nf">received?</span>
<span class="n">transfer_result</span> <span class="o">=</span> <span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:transfer_funds</span><span class="p">)</span>
<span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:send_email</span><span class="p">)</span> <span class="k">if</span> <span class="n">transfer_result</span><span class="p">.</span><span class="nf">ok?</span>
<span class="nb">sleep</span> <span class="mi">10</span><span class="p">.</span><span class="nf">days</span>
<span class="n">mark_payment_as_final</span>
</code></pre></div></div>

<p>we can also turn it into a DAG with a slightly different configuration (using our previous translation of this code into DAG nodes and connections):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">authorisation</span> <span class="o">=</span> <span class="n">dag</span><span class="p">.</span><span class="nf">create_node</span><span class="p">(</span><span class="ss">:authorise_payment</span><span class="p">)</span>
<span class="n">funds_check</span> <span class="o">=</span> <span class="n">dag</span><span class="p">.</span><span class="nf">create_node</span><span class="p">(</span><span class="ss">:check_funds</span><span class="p">)</span>

<span class="n">join</span> <span class="o">=</span> <span class="n">dag</span><span class="p">.</span><span class="nf">create_node</span><span class="p">(</span><span class="ss">:wait_for_all_inputs</span><span class="p">)</span>
<span class="n">join</span><span class="p">.</span><span class="nf">connect_inputs</span><span class="p">(</span><span class="n">authorisation</span><span class="p">,</span> <span class="n">funds_check</span><span class="p">)</span>

<span class="n">transfer_result</span> <span class="o">=</span> <span class="n">dag</span><span class="p">.</span><span class="nf">create_node</span><span class="p">(</span><span class="ss">:transfer_funds</span><span class="p">)</span>
<span class="n">transfer_result</span><span class="p">.</span><span class="nf">connect_inputs</span><span class="p">(</span><span class="n">join</span><span class="p">)</span>

<span class="n">email</span> <span class="o">=</span> <span class="n">dag</span><span class="p">.</span><span class="nf">create_node</span><span class="p">(</span><span class="ss">:send_email</span><span class="p">)</span>
<span class="c1"># We need some form of branching -</span>
<span class="c1"># this could be one of the possible ways</span>
<span class="n">email</span><span class="p">.</span><span class="nf">connect_inputs</span><span class="p">(</span><span class="n">transfer_result</span><span class="p">.</span><span class="nf">ok_output</span><span class="p">)</span> <span class="c1"># There would also be Node#error_output</span>

<span class="n">wait</span> <span class="o">=</span> <span class="n">dag</span><span class="p">.</span><span class="nf">create_sleep</span><span class="p">(</span><span class="mi">10</span><span class="p">.</span><span class="nf">days</span><span class="p">)</span>
<span class="n">wait</span><span class="p">.</span><span class="nf">connect_inputs</span><span class="p">(</span><span class="n">email</span><span class="p">)</span>

<span class="n">finalize</span> <span class="o">=</span> <span class="n">dag</span><span class="p">.</span><span class="nf">create_node</span> <span class="p">{</span> <span class="n">mark_payment_as_final</span> <span class="p">}</span>
<span class="n">finalize</span><span class="p">.</span><span class="nf">connect_inputs</span><span class="p">(</span><span class="n">wait</span><span class="p">)</span>
</code></pre></div></div>

<p>What do we lose in this instance? Simple: the ability to run 2 nodes (<code class="language-plaintext highlighter-rouge">check_funds</code> and <code class="language-plaintext highlighter-rouge">authorize_payment</code>) in parallel. Sometimes it can be a blocker, sometimes not. But what we gained is that we could turn our DAG into a <em>stack.</em></p>

<p>This has a number of massive, massive benefits:</p>

<ul>
  <li>We do not need to worry about “some failed” situations</li>
  <li>We do not need to do the topo sort - the steps always resolve top-down</li>
  <li>We do not have any fork-and-join problems</li>
</ul>

<p>And since our steps are a subset of a DAG: we can, later on, redesign our system in such a way that it will become able to do fork-and-join and be a DAG.</p>

<p>So: while your step workflow is going to be a DAG under the hood, it is feasible to design it so that you don’t need a DAG immediately.</p>

<h2 id="to-recap">To recap</h2>

<p>Most - if not all - “durable execution” systems will usually build on top of “workflows” where every “workflow” is actually a DAG. The amount of pretense that it is not so will depend on the sophistication of the developers of the workflow system in question.</p>

<p>Stay tuned for Part 3, where we explore how these “two worlds” - the DAG and its nodes - interact!</p>]]></content><author><name></name></author><summary type="html"><![CDATA[This is the next article in the series On the Way to Step Functions — you can find the first article in the series here. Previously, I have outlined the ambient desire in the field (marshalable stacks) and described why that is largely unachievable. But if imperative invocations can’t bid us consolation, what could? DAGs, in fact. If you are impatient (and a Rails user) - just head to the geneva_drive repo for the grand reveal. This post is part of a series: On the way to step functions: Part 1 On the way to step functions: Part 2 On the way to step functions: Part 3 On the way to step functions: Part 4]]></summary></entry><entry><title type="html">On the way to step functions: dreams of marshalable stacks</title><link href="https://blog.julik.nl/2026/01/on-the-way-to-step-functions-part-1" rel="alternate" type="text/html" title="On the way to step functions: dreams of marshalable stacks" /><published>2026-01-16T00:00:00+00:00</published><updated>2026-01-16T00:00:00+00:00</updated><id>https://blog.julik.nl/2026/01/on-the-way-to-step-functions-part-1</id><content type="html" xml:base="https://blog.julik.nl/2026/01/on-the-way-to-step-functions-part-1"><![CDATA[<p>Lately, it has been a great challenge to get into the world of durable execution. Granted, in the world of web applications we often do not need it. However, anyone who has come in contact with electronic payments, money orders, verifications - will likely encounter a need for durable execution.</p>

<p>The juggernauts in the space are, of course, <a href="https://temporal.io">Temporal.io</a> - with the recently emerging contender <a href="https://www.restate.dev/">restate</a> following in its footsteps. Both are based on the premise of sagas, controlled by a separate service.</p>

<p>There is also <a href="https://www.dbos.dev/">DBOS</a> and now there is also <a href="https://github.com/earendil-works/absurd">absurd</a> and the <a href="https://vercel.com/docs/workflow">Vercel Workflow</a>.</p>

<p>As it happens, meeting <a href="https://bou.ke/">Bouke</a> when I joined Cheddar has spurred my interest in Temporal. I knew someone who did work on the payments infrastructure at Uber (where Temporal came from), and intellectually the problems in the space are just very stimulating. However, as luck would have it, it would take another 2 years at Cheddar before a need would arise for actually <em>using</em> durable execution, in actual features. And in the course of implementing it, a few things turned out to be very invigorating indeed. So, before we go into any libraries or solutions: let’s just contemplate.</p>

<p>However, if you are impatient (and a Rails user) - just head to the <a href="https://github.com/julik/geneva_drive">geneva_drive</a> repo for the grand reveal.</p>

<p>This post is part of a series:</p>

<ul>
  <li><a href="/2026/01/on-the-way-to-step-functions-part-1">On the way to step functions: Part 1</a></li>
  <li><a href="/2026/01/on-the-way-to-step-functions-part-2">On the way to step functions: Part 2</a></li>
  <li><a href="/2026/01/on-the-way-to-step-functions-part-3">On the way to step functions: Part 3</a></li>
  <li><a href="/2026/02/on-the-way-to-step-functions-part-4">On the way to step functions: Part 4</a></li>
</ul>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>The premise is this. Imagine we have a payment workflow:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">from_account</span> <span class="o">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">payment_accounts</span><span class="p">.</span><span class="nf">first!</span>
<span class="c1"># The call to `.transfer` may retry or fail, and take an arbitrary long</span>
<span class="c1"># amount of time</span>
<span class="kp">loop</span> <span class="k">do</span>
  <span class="n">payment_status</span> <span class="o">=</span> <span class="n">payment_provider</span><span class="p">.</span><span class="nf">transfer</span><span class="p">(</span><span class="n">from_account</span><span class="p">:,</span> <span class="ss">to_account: </span><span class="n">recipient_account</span><span class="p">,</span> <span class="n">amount</span><span class="p">:,</span> <span class="n">idempotency_key</span><span class="p">:)</span>
  <span class="k">if</span> <span class="n">payment_status</span> <span class="o">!=</span> <span class="ss">:still_processing</span>
    <span class="k">break</span>
  <span class="k">end</span>
  <span class="nb">sleep</span> <span class="mi">30</span><span class="p">.</span><span class="nf">minutes</span> <span class="c1"># We want to suspend our code, completely - and resume from here later</span>
<span class="k">end</span>
</code></pre></div></div>

<p>What one wants here, is that the code being executed can actually be paused, and then resumed <em>at arbitrary points</em>. This is the same thing, effectively, as Terraform forcing its users to use a specifically designed <em>declarative language</em> - a declarative language does not impose a flow of execution, and externalises the places where the execution can be paused, retried or aborted. What Temporal tries desperately to do is to provide you an API which allows you to do this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">authorisation_result</span> <span class="o">=</span> <span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:authorise_payment</span><span class="p">)</span>
<span class="n">funds_check_result</span> <span class="o">=</span> <span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:check_funds</span><span class="p">)</span>
<span class="nb">sleep</span> <span class="k">until</span> <span class="n">authorisation_result</span><span class="p">.</span><span class="nf">received?</span> <span class="o">&amp;&amp;</span> <span class="n">funds_check_result</span><span class="p">.</span><span class="nf">received?</span>
<span class="n">transfer_result</span> <span class="o">=</span> <span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:transfer_funds</span><span class="p">)</span>
<span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:send_email</span><span class="p">)</span> <span class="k">if</span> <span class="n">transfer_result</span><span class="p">.</span><span class="nf">ok?</span>
<span class="nb">sleep</span> <span class="mi">10</span><span class="p">.</span><span class="nf">days</span>
<span class="n">mark_payment_as_final</span>
</code></pre></div></div>

<p>Now, if you have a runtime with sufficiently light userspace threads or co-routines - be it Go, BEAM or even Node - you may get this to work fairly easily. When you enter one of those <code class="language-plaintext highlighter-rouge">run_remotely_and_asynchronously</code> sections, your workload gets shipped off to a separate co-routine - or, even, to a co-routine that performs an RPC call. Your co-routine waits as long as it needs to in a sleep state, before getting awakened by an IO reactor or other signaling primitive. It then returns its result to the calling coroutine, which then can choose to either retry the action, perform rollback or do any other interesting steps.</p>

<p>If you are following the async-io world in Ruby you would even know by now that it is absolutely possible to write such code today, and if you do not restart your program - it will work, with decent resource consumption.</p>

<p>If your program never exits, and your server never crashes, and you never get network partitions - this is entirely possible to achieve. The challenge here lies in the fact that we are speaking about an “orchestrating program” - a body of instructions that should always, at any possible cost, run to completion.</p>

<p>That is, it should always terminate cleanly, or - at the very least - terminate out of “its own volition”, from within its own call stack.</p>

<p>Now, if you are running a digital telephony device - which runs Erlang - and this device is meant for 24/7 operation, months on end - without reboots or software updates - you may very well be onto something. If you are dealing with a phone call, for instance, it does - after all - likely end in some reasonable timeframe - probably within 24 hours. During that time, if your appliance crashes - the call will be disconnected anyhow. Even then - the Erlang motto of “just let it crash” is already hinting us at the fact that even when your “orchestrating program” is cheap to run – it does not give you a guarantee that it will never crash, and you should be prepared for this eventuality.</p>

<p>We, with our cloud functions nonsense, have functions which spin up at the flick of a wand - and providers pride themselves in being able to bring one up in microseconds, and to terminate one just as quickly. Something that would be an acceptable call (<code class="language-plaintext highlighter-rouge">sleep 30.minutes</code>) in an always-on long-lived system becomes a forbidding luxury in the cloud.</p>

<p>Moreover, some systems are just not fit for this because in addition to the actual variables involved in the stack frame, there is a whole context around the invocation - which has a non-zero cost. A load balancer somewhere is holding connection to the invocation. A browser is waiting for responses on the other end of that connection. Rails allocates a database connection to that invocation as soon as the first <code class="language-plaintext highlighter-rouge">SELECT * FROM users WHERE ...</code> gets done, and does not release that connection back into the pool until the invocation returns. Suspending that invocation for an arbitrary amount of time is not only costly in terms of keeping the actual values on the stack alive, but also in terms of holding on to all that context.</p>

<h2 id="electric-dreams-of-marshalable-stacks">Electric dreams of marshalable stacks</h2>

<p>All of those systems - Temporal, Restate and the ilk - are trying to create a runtime, where just one thing would be different from the actual state of various runtimes we already have. It would be a runtime where a function would be able to “snapshot” itself and put itself into “deep sleep”. When the time comes to “resume it” - usually by having the external scheduler (or orchestrator, or another function call) knowing “when” - the function would be allocated to a machine (or a VM, or an isolate, or a thread) - and magically “revived”, upon which it would resume execution from the spot in the call stack where it left off.</p>

<p>All the APIs I’ve seen for durable execution try to do their damnedest best to <em>pretend</em> that this is possible to achieve, yet in none of the runtimes is it actually possible. Therefore a <code class="language-plaintext highlighter-rouge">sleep 30.minutes</code> becomes <code class="language-plaintext highlighter-rouge">fancy_context.pause_for(30.minutes)</code>. Local variables necessarily become <code class="language-plaintext highlighter-rouge">fancy_context.store("authorisation_token", token)</code> and the like.</p>

<p>Granted, modern operating systems support hibernation pretty well, with two caveats: on the same machine with the same hardware, and without changes to the program. There is some support for VM snapshotting, but those snapshots are <em>big</em> - they are the whole kit and kitchen sink: the OS, the libraries, and the entire contents of memory. But even then: a marshalable stack would be quite a feat to pull off.</p>

<p>The main obstacle with a marshalable stack - which can also be relocated between machines - would be the <em>handles.</em> See, most of things in programs that do <em>useful stuff</em> - like accessing files, sockets, databases, GPUs, interrupts - they are what is called a “handle” - some “opaque reference to an externally provided resource”. And most of those are not marshalable. Remember how - by far - not everything in a Java program necessarily implements <code class="language-plaintext highlighter-rouge">Serializable</code>? Things which do not - those would just get tossed when serializing, but then one would need to have a way to revive them. A language with truly serializable stacks would likely implement such a construct:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">SerializableDatabaseConnection</span>
  <span class="k">def</span> <span class="nf">to_hibernated_handle</span>
    <span class="n">connection_configuration</span><span class="p">.</span><span class="nf">to_h</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">revive_from_hibernated_handle</span><span class="p">(</span><span class="n">connection_configuration</span><span class="p">)</span>
    <span class="n">connect</span><span class="p">(</span><span class="n">connection_configuration</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>But handles are only a part of the problem. Code changes too. If our hibernating invocations, which were stored a couple of months ago, are still expecting our system to have <code class="language-plaintext highlighter-rouge">PaymentInterfaceV1</code> - it will be a loud “bang!” when they get resumed and discover there is no longer such a thing - as it has been replaced by <code class="language-plaintext highlighter-rouge">PaymentInterfaceAdapterFactory</code>. Every code change, every deployment would become an exercise of verifying whether any hibernating invocations “on file” will still find their requisite primitives when revived.</p>

<p>The closest point where a Rails developer would encounter such a problem would be when you are deploying a new version of an <code class="language-plaintext highlighter-rouge">ActiveJob</code> subclass, or some code that is using such a subclass. Once you have a job for <code class="language-plaintext highlighter-rouge">PerformPaymentJob</code> in your queue - by the time that job should run you better damn have a class for <code class="language-plaintext highlighter-rouge">PerformPaymentJob</code> defined in your application, or else.</p>

<p>I suspect this mostly to be the reason hibernation (and thus - marshalable stacks) did not take hold in the cloud world. Even Smalltalk supported machine images, but these machine images were exactly what they are called - <em>machine images</em> (the invocation and the kitchen sink) - not <em>method images</em> - they were big, and would offer you a state of an entire system “ab initio”. I don’t know how you see keeping a Docker container in hibernation, with all of its 16GB of storage and memory, for every payment that you initiate. And besides, doing such a feat as marshalable invocations would mean that one needs a new language!</p>

<p>Not only because the primitives - or modules - of the host system could change between resumptions. But also because there are constructs which become very precarious if you try to see them within the same context of execution, but that context would get suspended - and then resumed in another machine. What would the following code block produce?</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">t1</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_MONOTONIC</span><span class="p">)</span>
<span class="c1"># here we hibernate our invocation for 2 hours</span>
<span class="n">delta</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_MONOTONIC</span><span class="p">)</span> <span class="o">-</span> <span class="n">t1</span>
</code></pre></div></div>

<p>Imagine that, by some feat of magic, we do have our distributed language that is a superset of Ruby and allows us to have those “hibernated invocations”. While we <em>can</em> (and will, of course) store <code class="language-plaintext highlighter-rouge">t1</code> - by virtue of it being on the stack - we won’t have a guarantee that the monotonic clock will be continuing exactly where it froze at hibernation time. We could have such a guarantee - but again, at the cost of retaining more state (the value of the monotonic clock at first invocation + the wall clock at the same instant). That clock, in turn, must - of course - be of such sizing (width) as to survive getting suspended for days - or months - without losing precision.</p>

<h2 id="idempotency-as-substitute">Idempotency as substitute</h2>

<p>Pretending that we can have a marshalable stack (or a serializable continuation, if you wish) is just so appealing, but inachievable in practice. What can we do instead? Well, we could say that our invocation is idempotent. That is: if we start it, and at some arbitrary point we want to suspend it (or it crashes) - there is a reasonable checkpoint someplace that we can <em>skip to</em>. Not <em>resume from</em> - and this is a very important difference - <em>skip to.</em> Imagine our library for durable execution wants to support the same bit of code we have written out earlier:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">authorisation_result</span> <span class="o">=</span> <span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:authorise_payment</span><span class="p">)</span>
<span class="n">funds_check_result</span> <span class="o">=</span> <span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:check_funds</span><span class="p">)</span>
<span class="nb">sleep</span><span class="p">()</span> <span class="k">until</span> <span class="n">authorisation_result</span><span class="p">.</span><span class="nf">received?</span> <span class="o">&amp;&amp;</span> <span class="n">funds_check_result</span><span class="p">.</span><span class="nf">received?</span>
<span class="n">transfer_result</span> <span class="o">=</span> <span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:transfer_funds</span><span class="p">)</span>
<span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:send_email</span><span class="p">)</span> <span class="k">if</span> <span class="n">transfer_result</span><span class="p">.</span><span class="nf">ok?</span>
<span class="nb">sleep</span> <span class="mi">10</span><span class="p">.</span><span class="nf">days</span>
<span class="n">mark_payment_as_final</span>
</code></pre></div></div>

<p>Let’s focus on just a small fragment of it:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">authorisation_result</span> <span class="o">=</span> <span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:authorise_payment</span><span class="p">)</span>
<span class="n">funds_check_result</span> <span class="o">=</span> <span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:check_funds</span><span class="p">)</span>
</code></pre></div></div>

<p>If we want to use this “as if it were idempotent” in a convenient manner, here is what we would likely do:</p>

<ul>
  <li>At invocation of <code class="language-plaintext highlighter-rouge">run_remotely_and_asynchronously</code> we would check whether there is a saved checkpoint for this invocation, keyed with the invocation ID – and, possibly, its arguments. If it exists - we would skip the call altogether and lookup the result of the call in some form of cache.</li>
  <li>If there is no checkpoint - we would create one, and spin up the <code class="language-plaintext highlighter-rouge">run_remotely_and_asynchronously</code> - either remotely or locally.</li>
</ul>

<p>Then, on the first invocation, our <code class="language-plaintext highlighter-rouge">run_remotely_and_asynchronously(:authorise_payment)</code> may succeed, but the program may crash before having reached <code class="language-plaintext highlighter-rouge">run_remotely_and_asynchronously(:check_funds)</code>. When we try to reinvoke our invocation again, it would see that there already is a checkpoint (and a saved return value!) for <code class="language-plaintext highlighter-rouge">run_remotely_and_asynchronously(:authorise_payment)</code> and would allow the program to skip over directly to <code class="language-plaintext highlighter-rouge">run_remotely_and_asynchronously(:check_funds)</code>. With any luck (and a lot of grease and duct tape) a somewhat sufficient API can be put together that it can “paper over” the absence of marshalable stacks.</p>

<p>But not all is roses here. A single argument that is not idempotent (or non-deterministic) passed to <code class="language-plaintext highlighter-rouge">run_remotely_and_asynchronously</code> would make the checkpoint useless:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">authorisation_result</span> <span class="o">=</span> <span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:authorise_payment</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span><span class="ss">idempotency_key: </span><span class="no">SecureRandom</span><span class="p">.</span><span class="nf">uuid</span><span class="p">})</span>
</code></pre></div></div>

<p>Every time we invoke our <code class="language-plaintext highlighter-rouge">run_remotely_and_asynchronously</code> here upon reentering the invocation, a new <code class="language-plaintext highlighter-rouge">idempotency_key</code> value would be generated. If we ignore it - our checkpoint may return values which will not belong to this invocation proper. If we use it as part of our cache key - every time it changes it is going to produce a new authorisation result, thereby not allowing us to “skip forward”.</p>

<p>A likely workaround for this would be to implement <em>yet more</em> distinctions between, say, “transient context parts” and “persisted context parts” - that way the <code class="language-plaintext highlighter-rouge">payment_id</code> would be considered contributing to the cache key / checkpoint, but the <code class="language-plaintext highlighter-rouge">idempotency_key</code> would not. Yet more things to take into account.</p>

<p>With more sophisticated VM control - or source code analysis - we could even go as far as placing those “checkpoints” on the level of source lines. This is incredibly brittle, of course - even more brittle than “context keying” - because a mere addition of some comments, or a split of a statement into multiple lines - could mean making the checkpoint becomes useless.</p>

<p>Effectively, we are getting to the same issue that we had with the absence of marshalable stacks - but on the level of source code. The <em>code</em> may change while the context is retained, and the context will no longer make sense in relation to the code.</p>

<p>If you look closely at Temporal documentation (and at the various clients, and the distinction between Workflows and Activities) you will see this “longing for marshalable stacks” everywhere. The longing which never gets answered.</p>

<p>There is more, of course. Imagine we need to supply an authentication token when we call our services. The token is time-limited (it has an <code class="language-plaintext highlighter-rouge">exp</code> claim), <em>and</em> it also has a random component (in the form of a <code class="language-plaintext highlighter-rouge">nonce</code>). It may not be reused. We could place it on the workflow level (“the body of code where we can skip”):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">token</span> <span class="o">=</span> <span class="no">ExternalClient</span><span class="p">.</span><span class="nf">generate_auth_token</span>
<span class="n">authorisation_result</span> <span class="o">=</span> <span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:authorise_payment</span><span class="p">,</span> <span class="ss">auth: </span><span class="n">token</span><span class="p">)</span>
</code></pre></div></div>

<p>but when we reattempt the <code class="language-plaintext highlighter-rouge">run_remotely_and_asynchronously</code> we will reuse the <code class="language-plaintext highlighter-rouge">token</code> that got persisted. And the remote service will (rightfully) refuse the token - either because it is going to have expired by then, or because it still remembers the <code class="language-plaintext highlighter-rouge">nonce</code> from the previous call. Solution? Well, placing the <code class="language-plaintext highlighter-rouge">ExternalClient.genereate_auth_token</code> inside of our <code class="language-plaintext highlighter-rouge">run_remotely_and_asynchronously</code> of course:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">authorisation_result</span> <span class="o">=</span> <span class="n">run_remotely_and_asynchronously</span><span class="p">(</span><span class="ss">:authorise_payment</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">token</span> <span class="o">=</span> <span class="no">ExternalClient</span><span class="p">.</span><span class="nf">generate_auth_token</span>
<span class="k">end</span>
</code></pre></div></div>

<p>It is not an issue of “having closures be used as step functions” - this is exactly why having closures is so useful and desirable (albeit the <code class="language-plaintext highlighter-rouge">_remotely_</code> part becomes difficult that way) - closures are great. The issue is <em>always remembering</em> what should go into the “outer program” - the orchestrator - and what must be in the “inner” programs, the tasks. And ensuring that both the tasks are idempotent <em>and</em> the outer workflow - our main invocation.</p>

<p>So: papering over the absence of stack marshaling with idempotency does work, but it begets a lot of things you need to worry about, and they can be very intricate. Very, very intricate indeed. In other words: the “ambient idempotency and it will just work” is a leaky abstraction.</p>

<p>And we haven’t even discussed recovering from exceptions yet.</p>

<h2 id="bring-it-back-sing-it-back">Bring it back, sing it back</h2>

<p>Orchestrated rollbacks (reverting partially completed actions) is something one may or may not encounter. The case <a href="https://docs.restate.dev/guides/sagas">Restate brings up</a> is very appropriate, although it is not necessarily very common. The issue is that again - for appropriate recovery we need to know <em>where to start recovering from</em> - from which point in the program. We have found this out the hard way - with <a href="https://github.com/cheddar-me/gouda">gouda</a> where, if a server crashes, the background job - which you could see as one such “unit of work” - may get interrupted simply because the container gets killed by the OOM killer.</p>

<p>Or there is a deploy and the instance group manager gets sick of waiting for the machine to shutdown cleanly. Point is that this <code class="language-plaintext highlighter-rouge">InterruptException</code> would have to be “synthesized” after the fact, judging by the workload not having had heartbeats for some time. There would not be a piece of code that would magically “raise” it - we had to make such piece of code.</p>

<p>The rollback performed afterwards is not impossible - but also something to think about. I have tried (with all my might) to be very minimal in “non-atomicity” of operations that may need staged or partial rollbacks, because I know how tricky those can be. Could even say I was lucky. But we should not discount the fact that they are a part of durable execution.</p>

<h2 id="conclusion">Conclusion</h2>

<p>A lot of the “spirit” of durable execution comes from the desire to have a revivable call stack, or low-cost VM snapshots, with such snapshots being marshalable, and revivable on different hosts than the ones creating them. Most current “cloud” durable execution engines try to pretend this to be kind-of-how-it-works, but actually replace functioning snapshotting with forced idempotency. And that idempotency is largely on the developer - you, thus. It is not bad per se, but it is a limitation.</p>

<p>Also: they could have spent the VC millions actually developing a good language/VM for this instead of trying to pretend Go and Java are a good fit, but who am I to recommend.</p>

<p>Head on to <a href="/2026/01/on-the-way-to-step-functions-part-2">Part 2,</a> where we will examine how those systems can actually be constructed without a marshalable stack – and how systems from non-cloud domains implement it.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Lately, it has been a great challenge to get into the world of durable execution. Granted, in the world of web applications we often do not need it. However, anyone who has come in contact with electronic payments, money orders, verifications - will likely encounter a need for durable execution. The juggernauts in the space are, of course, Temporal.io - with the recently emerging contender restate following in its footsteps. Both are based on the premise of sagas, controlled by a separate service. There is also DBOS and now there is also absurd and the Vercel Workflow. As it happens, meeting Bouke when I joined Cheddar has spurred my interest in Temporal. I knew someone who did work on the payments infrastructure at Uber (where Temporal came from), and intellectually the problems in the space are just very stimulating. However, as luck would have it, it would take another 2 years at Cheddar before a need would arise for actually using durable execution, in actual features. And in the course of implementing it, a few things turned out to be very invigorating indeed. So, before we go into any libraries or solutions: let’s just contemplate. However, if you are impatient (and a Rails user) - just head to the geneva_drive repo for the grand reveal. This post is part of a series: On the way to step functions: Part 1 On the way to step functions: Part 2 On the way to step functions: Part 3 On the way to step functions: Part 4]]></summary></entry><entry><title type="html">Making Rails Global IDs safer</title><link href="https://blog.julik.nl/2025/12/a-trap-with-global-ids" rel="alternate" type="text/html" title="Making Rails Global IDs safer" /><published>2025-12-10T00:00:00+00:00</published><updated>2025-12-10T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/12/a-trap-with-global-ids</id><content type="html" xml:base="https://blog.julik.nl/2025/12/a-trap-with-global-ids"><![CDATA[<p>The new LLM world is very exciting, and I try to experiment with the new tools when I can. This includes building agentic applications, one of which is my personal accounting and invoicing tool - <a href="/2025/08/turning-apple-calendar-into-time-tracker">that I wrote about previously</a></p>

<p>As part of that effort I started experimenting with RubyLLM to have some view into items in my system. And while I have used a neat pattern for referencing objects in the application from the tool calls - the Rails Global ID system - it turned out to be quite treacherous. So, let’s have a look at where <code class="language-plaintext highlighter-rouge">GlobalID</code> may bite you, and examine alternatives and tweaks we can do.</p>

<h2 id="what-are-rails-gids">What are Rails GIDs?</h2>

<p>The Rails global IDs (“GIDs”) are string handles to a particular model in a Rails application. Think of it like a model URL. They usually have the form of <code class="language-plaintext highlighter-rouge">gid://awesome-app/Post/32</code>. That comprises:</p>

<ul>
  <li>The name of your app (roughly what you passed in when doing <code class="language-plaintext highlighter-rouge">rails new</code>)</li>
  <li>The class name of the model</li>
  <li>The primary key of the model</li>
</ul>

<p>You can grab a model in your application and get a global ID for it:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">moneymaker</span><span class="p">(</span><span class="n">dev</span><span class="p">):</span><span class="mo">001</span><span class="o">&gt;</span> <span class="no">Invoice</span><span class="p">.</span><span class="nf">last</span><span class="p">.</span><span class="nf">to_global_id</span>
  <span class="no">Invoice</span> <span class="no">Load</span> <span class="p">(</span><span class="mf">0.3</span><span class="n">ms</span><span class="p">)</span>  <span class="no">SELECT</span> <span class="s2">"invoices"</span><span class="p">.</span><span class="nf">*</span> <span class="no">FROM</span> <span class="s2">"invoices"</span> <span class="no">ORDER</span> <span class="no">BY</span> <span class="s2">"invoices"</span><span class="o">.</span><span class="s2">"id"</span> <span class="no">DESC</span> <span class="no">LIMIT</span> <span class="mi">1</span> <span class="o">/*</span><span class="n">application</span><span class="o">=</span><span class="s1">'Moneymaker'</span><span class="o">*</span><span class="sr">/
=&gt; #&lt;GlobalID:0x00000001415978a0 @uri=#&lt;URI::GID gid:/</span><span class="o">/</span><span class="n">moneymaker</span><span class="o">/</span><span class="no">Invoice</span><span class="o">/</span><span class="mi">161</span><span class="o">&gt;&gt;</span>
</code></pre></div></div>

<p>Rails uses those GIDs primarily in ActiveJob serialization. When you do</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">DebitFundsJob</span><span class="p">.</span><span class="nf">perform_later</span><span class="p">(</span><span class="n">customer</span><span class="p">)</span>
</code></pre></div></div>

<p>where the <code class="language-plaintext highlighter-rouge">customer</code> is your <code class="language-plaintext highlighter-rouge">Customer</code> model object which is stored in the DB, ActiveJob won’t serialize its attributes but instead serialize it as a “handle” - the global ID. When your job gets deserialized from the queue, the global ID is going to get resolved into a <code class="language-plaintext highlighter-rouge">SELECT</code> and your <code class="language-plaintext highlighter-rouge">perform</code> method will get the resulting <code class="language-plaintext highlighter-rouge">Customer</code> model as argument.</p>

<p>All very neat. And dangerous, sometimes - once LLMs become involved.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="basics-llm-tool-calls">Basics: LLM tool calls</h2>

<p>LLM tool calls are ways for the model to call your application and get actionable results. For example, you may have a tool which allows your model to search for all unpaid invoices:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Finds all invoices which are unpaid</span>
<span class="k">def</span> <span class="nf">call</span>
  <span class="n">unpaid_invoices</span> <span class="o">=</span> <span class="no">Current</span><span class="p">.</span><span class="nf">account</span><span class="p">.</span><span class="nf">invoices</span><span class="p">.</span><span class="nf">unpaid</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="ss">:id</span><span class="p">).</span><span class="nf">all</span>
  <span class="n">result</span> <span class="o">=</span> <span class="n">unpaid_invoices</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">invoice</span><span class="o">|</span>
    <span class="p">{</span><span class="ss">gid: </span><span class="n">invoice</span><span class="p">.</span><span class="nf">to_global_id</span><span class="p">}</span>
  <span class="k">end</span>
  <span class="no">JSON</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">result</span><span class="p">)</span> <span class="c1"># [{"gid": "gid://moneymaker/Invoice/32"}, {"gid": "gid://moneymaker/Invoice/45"}]</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Then, there may be another tool in your system which allows the model to get details about a particular invoice:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">invoice_gid</span><span class="p">)</span>
  <span class="n">invoice</span> <span class="o">=</span> <span class="no">GlobalID</span><span class="o">::</span><span class="no">Locator</span><span class="p">.</span><span class="nf">locate</span><span class="p">(</span><span class="n">invoice_gid</span><span class="p">)</span>
  <span class="no">JSON</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">invoice</span><span class="p">.</span><span class="nf">attributes</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Why are GIDs so appealing when working with LLMs? Well, LLMs work with text tokens. They are very adept at recognizing patterns, and they can be instructed to both read those tokens from tool calls and to construct them in-situ. For example, we can instruct the LLM in our tool to pass us invoice GIDs:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Use this tool to get details about an invoice. The argument of the tool is the invoice GID, which looks like:

gid://moneymaker/Invoice/32
</code></pre></div></div>

<p>And now comes the meat of the problem.</p>

<h2 id="when-gids-turn-deadly">When GIDs turn deadly</h2>

<p>When working with GIDs, it is important to remember three things:</p>

<ul>
  <li>GIDs are not guaranteed to be generated by your application code, as they are <em>trusted identifiers</em></li>
  <li>GIDs are not checked for authorization when doing the lookup - they are meant to be generated above the authorization layer, and to be consumed above the authorization layer</li>
  <li>GIDs use <code class="language-plaintext highlighter-rouge">ActiveRecord::Base.find</code></li>
</ul>

<p>That last one is important. My invoices also have a <code class="language-plaintext highlighter-rouge">system_identifier</code>, which is a UUID. A couple of tools do know about the existence of those identifiers. Once, I was stunned to have the tool find an invoice for me, but the tool actually pulled up <em>the wrong record.</em> It did not fail with a <code class="language-plaintext highlighter-rouge">RecordNotFound</code> or other exception, it actually <em>substituted</em> the UUID into a GID it has generated and passed to a tool call which worked similarly to <code class="language-plaintext highlighter-rouge">call(invoice_gid)</code>.</p>

<p>An investigation was done, and a very interesting quality of GIDs has become apparent. As I mentioned previously, the GIDs call into <code class="language-plaintext highlighter-rouge">find</code> under the hood - you can check the code for yourself in https://github.com/rails/globalid/blob/main/lib/global_id/locator.rb</p>

<p>And <code class="language-plaintext highlighter-rouge">ActiveRecord::Base.find</code> has a very interesting property. See, from its inception Rails valued proper, clean URLs like a necessary convenience. You should be able to have URLs like <code class="language-plaintext highlighter-rouge">posts/my-post-slug</code> and have them lookup effortlessly - which is a noble endeavour. But storing a separate <code class="language-plaintext highlighter-rouge">slug</code> and its index and whatnot seemed wasteful. Thus, a “holy pair” of methods has been devised:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">to_param</span>
  <span class="n">slug</span> <span class="o">=</span> <span class="n">title</span><span class="p">.</span><span class="nf">titleize</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/\s+/</span><span class="p">,</span> <span class="s2">""</span><span class="p">).</span><span class="nf">underscore</span><span class="p">.</span><span class="nf">dasherize</span> <span class="c1"># "Amazing post title" =&gt; "amazing-post-title"</span>
  <span class="p">[</span><span class="nb">id</span><span class="p">,</span> <span class="n">slug</span><span class="p">].</span><span class="nf">join</span><span class="p">(</span><span class="s2">"-"</span><span class="p">)</span> <span class="c1"># 761-amazing-post-title</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Then, the Rails finder methods would do something quite clever (pseudocode):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">find</span><span class="p">(</span><span class="nb">id</span><span class="p">)</span>
  <span class="n">just_int_id</span> <span class="o">=</span> <span class="nb">id</span><span class="p">.</span><span class="nf">to_i</span> <span class="c1"># "3-entry" =&gt; 3, extracts leading digits</span>
  <span class="n">where</span><span class="p">(</span><span class="ss">id: </span><span class="n">just_int_id</span><span class="p">).</span><span class="nf">first!</span>
<span class="k">end</span>
</code></pre></div></div>

<p>So you could feed your <code class="language-plaintext highlighter-rouge">"761-amazing-post-title"</code> - which is a String - to all the <code class="language-plaintext highlighter-rouge">find</code>-related methods and it would lookup record <code class="language-plaintext highlighter-rouge">761</code> (an Integer) for you instead. Which is neat, no doubt about it (and no, dear strong typing adepts, it is not horrifying - it is actually neat).</p>

<p>That said, if you allow arbitrary strings into those lookups - interesting things may happen. For example, the LLM may hallucinate that a GID for an invoice it needs to examine is actually not a composition of <code class="language-plaintext highlighter-rouge">&lt;model class&gt;/&lt;primary key&gt;</code>, but instead <code class="language-plaintext highlighter-rouge">&lt;model class&gt;/&lt;other identifier it associated with the invoice&gt;</code>. And here, an interesting thing will happen. Remember that Rails scans for digit sequences to infer the ID? Now watch:</p>

<ul>
  <li>The LLM hallucinates a GID with <code class="language-plaintext highlighter-rouge">gid://moneymaker/Invoice/22ecb3fd-5e25-462c-ad2b-cafed9435d16</code></li>
  <li>That GID then gets passed to the tool which looks up the invoice. Or to a view!</li>
  <li>ActiveRecord happily extracts the <em>first sequence of digits</em> from that UUID (using <code class="language-plaintext highlighter-rouge">scan(/\d+/)</code>), assuming this is an ID with a slug, and finds us… the invoice <code class="language-plaintext highlighter-rouge">22</code> - since “22” is the first digit sequence extracted from “22ecb3fd-5e25-462c-ad2b-cafed9435d16”.</li>
  <li>That found invoice then may get shown, may get modified - most anything, really. But, more importantly, it may belong to a different user!</li>
</ul>

<p>And - remember - that the GIDs have two other properties:</p>

<h2 id="gids-are-not-guaranteed-to-be-generated-by-your-application-code">Gids are not guaranteed to be generated by your application code</h2>

<p>A GID can be a freeform string that is composed anywhere - for example, hallucinated by an LLM. But when you turn it into a handle, you do not perform any verification on whether that GID has been actually <em>produced</em> by your code. Imagine you have an <code class="language-plaintext highlighter-rouge">Account</code>, and it belongs to a user. Another user is then using the system, and the LLM hallucinates a GID with an actually present ID of an account, but that is the account of someone else! And since GIDs are not signed - the GID produced by the LLM will be accepted “at face value”.</p>

<p>This means that resolving an arbitrary GID you get from an LLM creates potential for <em>information disclosure.</em></p>

<h2 id="gids-exist-above-your-authorization-layer">Gids exist above your authorization layer</h2>

<p>When you receive an ID for something that belongs to a <code class="language-plaintext highlighter-rouge">User</code>, you are likely to query for it like this in your controllers and models:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">current_user</span><span class="p">.</span><span class="nf">credit_statements</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">id_from_params</span><span class="p">)</span>
</code></pre></div></div>

<p>This automatically scopes the query to the current user, and prevents information disclosure. But you do not have that luxury when you do <code class="language-plaintext highlighter-rouge">GlobalID::Locator.locate(gid_string)</code> - it just does a primary key lookup. Remember that GIDs were made for facilitating ActiveJob serialization - they are a <em>system-level</em> facility, not a <em>product-level</em> facility. “Bare” GID lookups (“locations”) are, thus, by definition, unsafe.</p>

<h2 id="navigating-our-way-out">Navigating our way out</h2>

<p>Now, GIDs are <em>neat</em> also! They are a very nice way for an LLM to reference objects in the system it interfaces with. They are also very neat for recalling those objects into views and parts of the application which do something useful with the LLM output. But “as is” they are fundamentally unsafe. So, should you want to use GIDs with LLM calls (or in other “not quite authenticated” contexts), here is what you can do.</p>

<p>GIDs support a concept called “app” - that is the name of the application that contains the GID being referenced. In Rails, you can actually have <em>several</em> GID namespaces, which will be correctly used when you perform your <code class="language-plaintext highlighter-rouge">locate</code> calls. This can be used to install a special <code class="language-plaintext highlighter-rouge">Locator</code> just for use by LLMs, which will be subject to way more restrictions than the app-wide locator used for ActiveJob.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">LLMSafeLocator</span> <span class="o">&lt;</span> <span class="no">GlobalID</span><span class="o">::</span><span class="no">Locator</span><span class="o">::</span><span class="no">BaseLocator</span>
  <span class="k">def</span> <span class="nf">locate</span><span class="p">(</span><span class="n">gid</span><span class="p">,</span> <span class="n">options</span> <span class="o">=</span> <span class="p">{})</span>
    <span class="n">model_id</span> <span class="o">=</span> <span class="n">gid</span><span class="p">.</span><span class="nf">model_id</span><span class="p">.</span><span class="nf">to_s</span>
    <span class="k">raise</span> <span class="s2">"Malformed pkey in </span><span class="si">#{</span><span class="n">gid</span><span class="si">}</span><span class="s2">"</span> <span class="k">unless</span> <span class="n">model_id</span><span class="p">.</span><span class="nf">match?</span><span class="p">(</span><span class="sr">/^\d+$/</span><span class="p">)</span>
    <span class="k">super</span><span class="p">(</span><span class="n">gid</span><span class="p">,</span> <span class="n">options</span><span class="p">).</span><span class="nf">tap</span> <span class="k">do</span> <span class="o">|</span><span class="n">maybe_model</span><span class="o">|</span>
      <span class="n">authorize_access!</span><span class="p">(</span><span class="n">maybe_model</span><span class="p">)</span> <span class="k">if</span> <span class="n">maybe_model</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">locate_many</span>
    <span class="c1"># ... similar</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">authorize_access!</span><span class="p">(</span><span class="n">model</span><span class="p">)</span>
    <span class="k">return</span> <span class="k">unless</span> <span class="n">model</span><span class="p">.</span><span class="nf">respond_to?</span><span class="p">(</span><span class="ss">:owner</span><span class="p">)</span>
    <span class="k">unless</span> <span class="n">model</span><span class="p">.</span><span class="nf">owner</span> <span class="o">==</span> <span class="no">Current</span><span class="p">.</span><span class="nf">user</span>
      <span class="k">raise</span> <span class="s2">"Unauthorized access to </span><span class="si">#{</span><span class="n">model</span><span class="p">.</span><span class="nf">class</span><span class="si">}</span><span class="s2">#</span><span class="si">#{</span><span class="n">model</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="s2"> from </span><span class="si">#{</span><span class="no">Current</span><span class="p">.</span><span class="nf">user</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2">"</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This way, we enforce two things:</p>

<ul>
  <li>Remove the “magic” for <code class="language-plaintext highlighter-rouge">find</code> to avoid misinterpretation of our primary keys</li>
  <li>Add an authorization layer so that lookups done by the LLM will be forced to the authorization scope</li>
</ul>

<p>We then install our locator under a separate app ID:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># application.rb</span>
<span class="no">GlobalID</span><span class="o">::</span><span class="no">Locator</span><span class="p">.</span><span class="nf">use</span> <span class="ss">:tainted</span><span class="p">,</span> <span class="no">LLMSafeLocator</span><span class="p">.</span><span class="nf">new</span>
</code></pre></div></div>

<p>and we then add a method to our ApplicationRecord:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">to_tainted_global_id</span>
  <span class="no">SignedGlobalID</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="nb">self</span><span class="p">,</span> <span class="ss">app: </span><span class="s2">"tainted"</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Note that we are applying a <em>signed</em> global ID here, because we don’t want the LLM to be hallucinating these GlobalIDs for us. If you are feeling adventurous and want to permit the LLM to generate those GIDs anyway:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">to_tainted_global_id</span>
  <span class="no">GlobalID</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="nb">self</span><span class="p">,</span> <span class="ss">app: </span><span class="s2">"tainted"</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Then, the tool calls and prompts should hint the LLM that the GIDs will be signed:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>The IDs you receive and use as GIDs are _opaque_. Do not manipulate or decode them as you may damage the ID. They look like this:

eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL21vbmV5bWFrZXIvSW52b2ljZS8zMiIsImV4cCI6IjIwMjYtMDEtMTBUMTI6MjQ6NDQuMDQ3WiIsInB1ciI6ImRlZmF1bHQifX0=--397235ab0a0d32e1d29ed0e2f136b34f573244a4
</code></pre></div></div>

<p>or hint it that the GIDs it operates with will have our <code class="language-plaintext highlighter-rouge">tainted://</code> app identifier:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>The IDs you receive and carry are GIDs, and look like this:

gid://tainted/Invoice/12
</code></pre></div></div>

<p>and from your code return not the app’s default GIDs, but your “tainted” GIDs instead:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Finds all invoices which are unpaid</span>
<span class="k">def</span> <span class="nf">call</span>
  <span class="n">unpaid_invoices</span> <span class="o">=</span> <span class="no">Current</span><span class="p">.</span><span class="nf">account</span><span class="p">.</span><span class="nf">invoices</span><span class="p">.</span><span class="nf">unpaid</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="ss">:id</span><span class="p">).</span><span class="nf">all</span>
  <span class="n">result</span> <span class="o">=</span> <span class="n">unpaid_invoices</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">invoice</span><span class="o">|</span>
    <span class="p">{</span><span class="ss">gid: </span><span class="n">invoice</span><span class="p">.</span><span class="nf">to_tainted_global_id</span><span class="p">}</span>
  <span class="k">end</span>
  <span class="no">JSON</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">result</span><span class="p">)</span> <span class="c1"># [{"gid": "gid://tainted/Invoice/32"}, {"gid": "gid://tainted/Invoice/45"}]</span>
<span class="k">end</span>
</code></pre></div></div>

<p>and make sure your LLM only ever gets - and sends you - the “tainted” GIDs:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">invoice_gid_str</span><span class="p">)</span>
  <span class="n">gid</span> <span class="o">=</span> <span class="no">GlobalID</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">invoice_gid_str</span><span class="p">)</span>
  <span class="k">raise</span> <span class="s2">"The passed GID is not correctly scoped - it uses </span><span class="si">#{</span><span class="n">gid</span><span class="p">.</span><span class="nf">app</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2">"</span> <span class="k">unless</span> <span class="n">gid</span><span class="p">.</span><span class="nf">app</span> <span class="o">==</span> <span class="s2">"tainted"</span>

  <span class="n">invoice</span> <span class="o">=</span> <span class="no">GlobalID</span><span class="o">::</span><span class="no">Locator</span><span class="p">.</span><span class="nf">locate</span><span class="p">(</span><span class="n">gid</span><span class="p">)</span>
  <span class="no">JSON</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">invoice</span><span class="p">.</span><span class="nf">attributes</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>There is a caveat: if you call <code class="language-plaintext highlighter-rouge">locate</code> yourself, you want to do so through your Rails application locator, because the global locator will revert to your “system” locator if the <code class="language-plaintext highlighter-rouge">app</code> you supply it is not configured.</p>

<h2 id="or-even-better">Or even better</h2>

<p>Do not play with GIDs unless you strictly have to, and use <code class="language-plaintext highlighter-rouge">signed_id</code> instead, passing the value into the ActiveRecord relations:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># As output</span>
<span class="n">invoice</span><span class="p">.</span><span class="nf">signed_id</span><span class="p">(</span><span class="ss">purpose: </span><span class="s2">"llm"</span><span class="p">)</span>

<span class="c1"># As input</span>
<span class="n">current_user</span><span class="p">.</span><span class="nf">invoices</span><span class="p">.</span><span class="nf">find_signed</span><span class="p">(</span><span class="n">signed_invoice_id</span><span class="p">,</span> <span class="ss">purpose: </span><span class="s2">"llm)
</span></code></pre></div></div>

<p>This is not as polymorphic but much safer in the long run.</p>

<h2 id="to-summarize">To summarize</h2>

<p>GlobalID is a neat concept in Rails, but it is not very safe - if you allow your LLMs to touch it, relatively severe security consequences can take place - ranging from information disclosure to data exfiltration. Should you choose to use them - do so with guardrails. And, in general, treat your LLM input into your tools as <em>untrusted user input,</em> with all that entails.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[The new LLM world is very exciting, and I try to experiment with the new tools when I can. This includes building agentic applications, one of which is my personal accounting and invoicing tool - that I wrote about previously As part of that effort I started experimenting with RubyLLM to have some view into items in my system. And while I have used a neat pattern for referencing objects in the application from the tool calls - the Rails Global ID system - it turned out to be quite treacherous. So, let’s have a look at where GlobalID may bite you, and examine alternatives and tweaks we can do. What are Rails GIDs? The Rails global IDs (“GIDs”) are string handles to a particular model in a Rails application. Think of it like a model URL. They usually have the form of gid://awesome-app/Post/32. That comprises: The name of your app (roughly what you passed in when doing rails new) The class name of the model The primary key of the model You can grab a model in your application and get a global ID for it: moneymaker(dev):001&gt; Invoice.last.to_global_id Invoice Load (0.3ms) SELECT "invoices".* FROM "invoices" ORDER BY "invoices"."id" DESC LIMIT 1 /*application='Moneymaker'*/ =&gt; #&lt;GlobalID:0x00000001415978a0 @uri=#&lt;URI::GID gid://moneymaker/Invoice/161&gt;&gt; Rails uses those GIDs primarily in ActiveJob serialization. When you do DebitFundsJob.perform_later(customer) where the customer is your Customer model object which is stored in the DB, ActiveJob won’t serialize its attributes but instead serialize it as a “handle” - the global ID. When your job gets deserialized from the queue, the global ID is going to get resolved into a SELECT and your perform method will get the resulting Customer model as argument. All very neat. And dangerous, sometimes - once LLMs become involved.]]></summary></entry><entry><title type="html">What does “intuitive” even mean?</title><link href="https://blog.julik.nl/2025/10/what-does-intuitive-even-mean" rel="alternate" type="text/html" title="What does “intuitive” even mean?" /><published>2025-10-31T00:00:00+00:00</published><updated>2025-10-31T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/10/what-does-intuitive-even-mean</id><content type="html" xml:base="https://blog.julik.nl/2025/10/what-does-intuitive-even-mean"><![CDATA[<p>Two remarkable posts on HN today: <a href="https://news.ycombinator.com/item?id=45761445">the new release of Affinity is now free and Canva-subsidized</a> and <a href="https://news.ycombinator.com/item?id=45760878">Free software scares normal people</a></p>

<p>There is a bit to unpack as to why these two are related: the first is about Affinity, which is a remarkable and very feature-complete suite of applications for graphics, and the other is about Handbrake having an “intimidating” UI. The two are in perfect connection, for an important reason: we often parade “intuitiveness” as a virtue, but with applications that are <em>tools</em> very few people take the effort to unpack what that coveted <em>intuitiveness</em> is. Or should be.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="intuitiveness-is-playing-to-habit">Intuitiveness is playing to habit</h2>

<p>What is called “intuition” is, often, making guesses which turn out to be right. As far as tools go, you make a guess about a tool and you turn out to be right - the tool is, thus, considered “intuitive”.</p>

<ul>
  <li>A pistol grip on a cordless drill implies that you can point it at the object (a screw or a spot where you want to drill a hole) and squeeze the trigger to make it go</li>
  <li>A button on a control panel has the same keycap shape as the button on your keyboard - you therefore know that it can be pressed and it is likely a non-sticky button</li>
  <li>A power plug on your appliance has two (or three) prongs just like any other power plug in your house. You know that you can stick it into a power outlet and nothing bad will happen (see also - <a href="https://x.com/CursedCables">@CursedCables</a> on X).</li>
</ul>

<p>There is nothing particularly <em>intuitive</em> about those tools that is “magical” - it is rather the fact that they are <em>predictably like other tools.</em> That is: if the user comes with a particular set of habits, they are likely to be able to accomplish the tasks they need with a given tool using this set of habits - the tool is, therefore, intuitive.</p>

<p>It is not that it is <em>designed to delight.</em> It is not that it <a href="https://en.wikipedia.org/wiki/Squircle">uses the squircle</a> of exactly the right shape which makes it intuitive. It is the fact that <em>most other apps on that particular platform also use squircles</em> which does.</p>

<p>The user has a habit, and playing into that habit makes things intuitive for them.</p>

<h2 id="do-thing-is-not-intuitive">“Do thing” is not intuitive</h2>

<p>In his post Daniel posits that the workings of Handbrake can be condensed into a drop well and a button called (I digress) “Do Thing”. The app is <a href="https://danieldelaney.net/magicbrake/">Magicbrake</a> - and it’s good. Really.</p>

<p>But it is <em>not</em> intuitive. Let me explain.</p>

<p>It is intuitive in the UI controls it uses - some people still remember how to use a macOS drop well (although you would be surprised how much less common it has become, primarily due to the Electron dominance). And there is a button - fair enough. But the button says “Make small, fast and compatible”.</p>

<p>Here’s the problem: these terms have no established habit behind them. To be somewhat inquisitive, what do the statements actually mean?</p>

<ul>
  <li>“Small” - what is “small”? It fits on a floppy drive? It will be below 10MB? It will be below 1MB per second of video?</li>
  <li>“Fast” - Fast <em>what?</em> Will the conversion speed up the video? Will it decode faster on a CPU? on a GPU? on a specific device?</li>
  <li>“Compatible” - what is “compatible” in this case? Compatible with what? QuickTime Player? VLC? iOS? Android? Plex?</li>
</ul>

<p>None of this is <em>intuitive</em> - it is <em>curated.</em> And it is curated in a very non-transparent manner, with very unclear definitions of what it was curated for. Is it simple? For a given definition of “small, fast and compatible” - probably. Is it intuitive? Not unless the user already has a habit of using “small, fast and compatible” buttons in other video conversion tools. To me – not really.</p>

<p>Moreover, using terms like “fast” where their definition is clearly not well-conceived and not applicable hints at what I call “product management feast” - invent something opaque, slap a marketing term on it, drive it through under the moniker “it just works”.</p>

<p>If the goal is to make something “intuitive”, having a single button to “Do thing” is not the way to do it.</p>

<h2 id="work-exactly-like-the-other-thing-is-intuitive">“Work exactly like the other thing” is intuitive</h2>

<p>A while ago I have experimented using the <a href="https://www.affinity.studio/">Affinity suite</a> instead of the Adobe applications I was used to for more than 30 years. And, in many ways, they have done a <em>marvelous</em> job on making it intuitive.</p>

<p>Did they replace the 100 UI elements with a single button called “Do Thing”? No.</p>

<p>Did they invent a new wizard which replaces cumbersome flows like layer duplication or type fitting? Also no.</p>

<p>They did something else: they have implemented 85% of the controls, workflows and names <em>exactly</em> the same as InDesign did it. Blatantly, unabashedly copied them down to the type editing shortcuts which diverge from the standard macOS shortcuts. For example, <code class="language-plaintext highlighter-rouge">⌥+⬅️</code> decreases the tracking distance between characters instead of “Emacs-unselect-word”. There is a hundred minute details where they took the time to <em>copy</em>, and <em>literally so,</em> most of the smallest interaction modes and shortcuts.</p>

<p>Moreover, as far as <em>intuitiveness</em> goes - the spots where Affinity Publisher, for one, turned out <em>not</em> to be intuitive, was in the workflows where they have diverged significantly from the InDesign model of doing things - for instance, in their treatment of placed images. The workflow in Affinity Publisher is very different with respect to those - and I found it not work very well. For example:</p>

<ul>
  <li>Instead of scaling the “image frame” inside the “bounding frame” using a bounding box, you have to use a slider and a drag hotspot</li>
  <li>There is auto-fit which is not very well indicated as you work your layout - you have to check a palette or a modal for its state</li>
  <li>It would seem that positioning the image inside a frame happens at a different scale mapping (screen cursor movement to page distance) than other interactions on the page</li>
</ul>

<p>It might be that some PM at Affinity decided that applying auto-fit to placed graphics would be <em>intuitive</em> - but they have, in fact, achieved the opposite.</p>

<p><em>Non-intuitive</em> is doing things <em>differently than everybody else,</em> because with your unique snowflake interaction the user can’t rely on <em>habit.</em></p>

<p>When habit is not usable, an emotional aspect comes into play that we can’t neglect either: if you show a person that “having done X this way for decades, you are now deprived of your muscle memory and you have to relearn everything because I, the software author, know better” can be very vexing. And it is not only about being told that your skills have become obsolete - it is also about being told that the skills you possessed, or learned, have become obsolete because the “new”, “improved” way of doing things is clearly superior to the one you have memorized and relied upon.</p>

<p>If the software you have mastered is complex and effective in what it does, it feels incredibly unfair that you have to re-learn everything just because some talented PM on the new Team Twample at Softco has decided to “change things up a bit”.</p>

<p>Another example of the “be like the others” is, of course, Blender. 3D graphics apps are known for having their own, very different and very special UI paradigms. Some apps use MMB-drag. Some apps use spacebar-drag. Some use Y-up, some use Z-up. Some have customizable interfaces with floating panels, some - have only panes. Some have several modes for selection, and some have one.</p>

<p>Blender, for a very long time, was known for forcing its users to learn a UI which was extremely divergent from all the other apps on the market (Maya, 3DSMax and even Houdini and Softimage) in pretty much every way possible. For the longest time the authors continued persevering in their vision of “we are doing it right, and everybody else is doing it wrong” - partly because of legacy code, partly because of their own habit.</p>

<p>But at a certain moment the Blender foundation understood that the key to popularity - and widespread adoption - is not robbing their users of habit. It is <em>being like everybody else.</em> They aligned their UI with industry-standard workflows and shortcuts, making it possible for users trained on Maya or 3DSMax to transfer their habits directly. The results speak for themselves: Blender adoption is soaring, and features are being worked in it - something we could not even fathom just a decade ago.</p>

<h2 id="creating-intuitive-experiences-requires-humility">Creating intuitive experiences requires humility</h2>

<p>I would recommend watching <a href="https://www.youtube.com/watch?v=QYM3TWf_G38">this video</a> to understand the tradeoffs involved when changing feature-full applications. The interesting aspect of it is that you would say that Tantacrul is driving the UX of both MuseScore and Audacity in the exact opposite direction of what “intuitive” means! There are more features, more workflows, and more modes!</p>

<p>Yet – nothing is further from the truth. Making something <em>intuitive</em> requires <em>humility.</em> It takes humility to admit “No, I can’t make this magical and dumb this down to the level of a Duplo set so that their grandma could use it - this is an application which serves a purpose, and by trying to create Delightful Magic I will make it unfit for that purpose”.</p>

<p>Which is exactly the deliberation that Tantacrul highlights in his videos, time and time again.</p>

<h2 id="in-summary">In summary</h2>

<p>If you want to make an intuitive application - do what everybody else is doing. Most of the time it is going to be exactly what is needed. Do not dumb down the tools, do not destroy people’s workflows - but be humble in what, how and why you curate and design. If you find yourself with an urge to create something “magical” - slap yourself on the wrist immediately.</p>

<p>You are not the deity to decide what “Do Thing” is supposed to be. Even Apple has failed at this repeatedly - remember Final Cut Pro X’s initial release, or the iWork redesigns that stripped out features professionals relied on? They eventually learned their lesson (Final Cut regained professional respect, Pages and Numbers restored missing features), but the cost was user frustration and lost trust. You don’t want to be in that company.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Two remarkable posts on HN today: the new release of Affinity is now free and Canva-subsidized and Free software scares normal people There is a bit to unpack as to why these two are related: the first is about Affinity, which is a remarkable and very feature-complete suite of applications for graphics, and the other is about Handbrake having an “intimidating” UI. The two are in perfect connection, for an important reason: we often parade “intuitiveness” as a virtue, but with applications that are tools very few people take the effort to unpack what that coveted intuitiveness is. Or should be.]]></summary></entry><entry><title type="html">Delete your old migrations, today</title><link href="https://blog.julik.nl/2025/10/delete-your-old-migrations" rel="alternate" type="text/html" title="Delete your old migrations, today" /><published>2025-10-07T00:00:00+00:00</published><updated>2025-10-07T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/10/delete-your-old-migrations</id><content type="html" xml:base="https://blog.julik.nl/2025/10/delete-your-old-migrations"><![CDATA[<p>We get attached to code - sometimes to a fault. Old migrations are exactly that. They’re digital hoarding at its finest, cluttering up your codebase with files that serve absolutely no purpose other than to make you feel like you’re preserving some kind of historical record.</p>

<p>But here’s the brutal truth: your old migrations are utterly useless. They’re worse than useless - they’re actively harmful. They’re taking up space, they are confusing (both for you and new developers on the project), and they give you a false sense of security about your database’s evolution.</p>

<p>If your database is out-of-sync with <code class="language-plaintext highlighter-rouge">schema.rb</code> you need to solve that problem anyway, and - if anything - the migrations make that problem worse.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="why-your-old-migrations-are-completely-useless">Why your old migrations are completely useless</h2>

<p>Let me repeat without mincing: <strong>your old migrations are not useful to you.</strong> I don’t care if they’re from 2010 or 2020 - they serve no purpose in your current codebase.</p>

<p>Here’s why:</p>

<p><strong>You either run all of them or none.</strong> Your database schema has evolved beyond recognition. Those migrations that created tables with different column names, different constraints, different indexes? They’are likely to fail if the previous migration hasn’t succeeded. The foreign key constraints may break, the column types may be wrong, and you’ll spend more time debugging migration failures than it would take to just recreate your schema from scratch.</p>

<p><strong>You’ll never run them from the beginning of history.</strong> This is the most delusional part of keeping old migrations - the idea that someday you might need to recreate your entire database from migration zero. When was the last time you actually did <code class="language-plaintext highlighter-rouge">rails db:drop db:create db:migrate</code> on a production system? Never, that’s when. You use <code class="language-plaintext highlighter-rouge">db:schema:load</code> or <code class="language-plaintext highlighter-rouge">db:structure:load</code> because it’s faster, more reliable, and doesn’t depend on the fragile chain of historical migrations.</p>

<p>And no, “keeping your migrations runnable from 0 to bring up the application” is not “clean engineering”. It is not a “good engineering practice”. It is bollocks. Just delete them.</p>

<p><strong>Your environment will never match.</strong> Even if you wanted to run those old migrations, your Rails version has changed, your gems have changed, your database version has changed. The migration that worked perfectly in 2018 with Rails 5.2 and PostgreSQL 10 can explode in a spectacular fashion with Rails 7.2 and PostgreSQL 16. Moreover: migrations sometimes use code in the application. Yes, it is discouraged, and yes, you will have more than a few which will invariably use code from the application or gems (or Rails itself). That <code class="language-plaintext highlighter-rouge">UserTagging</code> model? Nowhere in sight. That neat <code class="language-plaintext highlighter-rouge">acts_as_listicle</code>? Out of maintenance and no longer available.</p>

<p><strong>They are only useful for archeology.</strong> You have <code class="language-plaintext highlighter-rouge">git</code> for that.</p>

<h2 id="how-to-delete-your-old-migrations">How to delete your old migrations</h2>

<p>First, make sure you commit your <code class="language-plaintext highlighter-rouge">schema.rb</code> or <code class="language-plaintext highlighter-rouge">structure.sql</code>. This is your source of truth now, not those ancient migration files.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Make sure this is up to date</span>
<span class="n">rails</span> <span class="n">db</span><span class="ss">:schema:dump</span>
<span class="n">git</span> <span class="n">add</span> <span class="n">db</span><span class="o">/</span><span class="n">schema</span><span class="p">.</span><span class="nf">rb</span>
<span class="n">git</span> <span class="n">commit</span> <span class="o">-</span><span class="n">m</span> <span class="s2">"Update schema before migration cleanup"</span>
</code></pre></div></div>

<p>Now, decide on a meaningful threshold. I recommend keeping migrations from the last 6-12 months, depending on how active your development is. Everything older than that is just digital clutter.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Find migrations older than 6 months and delete them</span>
git ls-tree <span class="nt">-r</span> <span class="nt">--name-only</span> HEAD | <span class="nb">grep</span> <span class="s2">"^db/migrate/"</span> | <span class="k">while </span><span class="nb">read </span>file<span class="p">;</span> <span class="k">do
  </span><span class="nv">last_commit</span><span class="o">=</span><span class="si">$(</span>git log <span class="nt">-1</span> <span class="nt">--format</span><span class="o">=</span><span class="s2">"%ct"</span> <span class="nt">--</span> <span class="s2">"</span><span class="nv">$file</span><span class="s2">"</span><span class="si">)</span>
  <span class="nv">cutoff</span><span class="o">=</span><span class="si">$(</span><span class="nb">date</span> <span class="nt">-v-180d</span> +%s<span class="si">)</span>
  <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$last_commit</span><span class="s2">"</span> <span class="nt">-lt</span> <span class="s2">"</span><span class="nv">$cutoff</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$file</span><span class="s2">"</span>
  <span class="k">fi
done</span> | xargs <span class="nt">-I</span> <span class="o">{}</span> git <span class="nb">rm</span> <span class="o">{}</span>

<span class="c"># ...and then</span>
git commit <span class="nt">-m</span> <span class="s2">"Delete old migrations - schema.rb is the source of truth"</span>
</code></pre></div></div>

<h2 id="the-psychological-barrier">The psychological barrier</h2>

<p>I know what you’re thinking: “But what if I need to understand how the schema evolved?”</p>

<p>You won’t. You really won’t. If you need to understand the current state of your database, look at <code class="language-plaintext highlighter-rouge">schema.rb</code>. If you need to understand the history, look at your Git history. If you need to understand the business logic, look at your models and tests.</p>

<p>The migration files themselves are just implementation details of how you got from point A to point B. The important thing is where you are now, not the 47 different ways you tried to add a column before you got it right.</p>

<h2 id="what-about-production-deployments">What about production deployments?</h2>

<p>This is where people get scared. “But what if I need to deploy to a fresh production server?” If you will – everything is going to be topsy-turvy, you run <code class="language-plaintext highlighter-rouge">rails db:schema:load</code> or <code class="language-plaintext highlighter-rouge">rails db:structure:load</code> and presto! Your production database gets created from your schema file, not from running 200 migrations in sequence. A migration is a brittle piece of code, which relies on 2 things being stable - the entire state of your codebase and the entire state of your database. You want to minimize the amount of time where these 2 guarantees must be maintained.</p>

<h2 id="the-only-exception">The only exception</h2>

<p>There’s exactly one scenario where you might want to keep old migrations: if you’re using a gem which installs its own migrations. They will still be recorded in your schema/structure, but when you run <code class="language-plaintext highlighter-rouge">bin/rails some_gem_with_database_tables:update</code> or <code class="language-plaintext highlighter-rouge">bin/rails some_gem_with_database_tables:install</code> it is likely that the gem will try upgrading its schema by outputting migrations into your source tree. It will also <em>skip</em> those migrations if you already have them (the generator will offer you a dialog for “overwrite/skip/merge”. You likely want to keep those.</p>

<h2 id="just-do-it">Just do it</h2>

<p>Your old migrations are digital hoarding. They’re taking up space, confusing people, and serving no purpose. Delete them today. No, they will not run if you try them now. And your AI agents will eat through mountains of tokens trying to make that migration from 2012 work.</p>

<p>Delete them. You’ll feel better. Your codebase will be cleaner. New developers will thank you. And you’ll never miss them. I promise.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[We get attached to code - sometimes to a fault. Old migrations are exactly that. They’re digital hoarding at its finest, cluttering up your codebase with files that serve absolutely no purpose other than to make you feel like you’re preserving some kind of historical record. But here’s the brutal truth: your old migrations are utterly useless. They’re worse than useless - they’re actively harmful. They’re taking up space, they are confusing (both for you and new developers on the project), and they give you a false sense of security about your database’s evolution. If your database is out-of-sync with schema.rb you need to solve that problem anyway, and - if anything - the migrations make that problem worse.]]></summary></entry><entry><title type="html">The little Random that could</title><link href="https://blog.julik.nl/2025/10/the-little-random-that-could" rel="alternate" type="text/html" title="The little Random that could" /><published>2025-10-05T00:00:00+00:00</published><updated>2025-10-05T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/10/the-little-random-that-could</id><content type="html" xml:base="https://blog.julik.nl/2025/10/the-little-random-that-could"><![CDATA[<p>Sometimes, after a few pints in a respectable gathering of Rubyists, someone will ask me “what is the most undervalued module in the Ruby standard library?”</p>

<p>There are many possible answers, of course, and some favoritism is to be expected. Piotr Szotkowski, who untimely passed away this summer, did <a href="https://www.youtube.com/watch?v=CITMkGs4BYI">a wonderful talk on the topic</a> a wee while back.</p>

<p>My personal answer to that question, however, would be <code class="language-plaintext highlighter-rouge">Random</code>. To me, <code class="language-plaintext highlighter-rouge">Random</code> is a unsung hero of a very large slice of the work we need to do in web applications, especially so when we need things to be deterministic and testable. So, let’s examine this little jewel a bit closer.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="the-various-random-sources">The various random sources</h2>

<p>A <code class="language-plaintext highlighter-rouge">Random</code> and its far-removed cousin <code class="language-plaintext highlighter-rouge">SecureRandom</code> are interfaces to random number generators, or <em>sources of entropy.</em> I won’t bother you with too many details, but the simplest use for an RNG is simulating a dice roll:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">random_side</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">rand</span><span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">6</span><span class="p">)</span>
</code></pre></div></div>

<p>You can also do the same using <code class="language-plaintext highlighter-rouge">SecureRandom</code> of course (there is quite a bit of overlap between <code class="language-plaintext highlighter-rouge">Random</code> and <code class="language-plaintext highlighter-rouge">SecureRandom</code> but not all of it):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">random_side</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">rand</span><span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">6</span><span class="p">)</span>
</code></pre></div></div>

<p>There is a substantial difference between them, though. <code class="language-plaintext highlighter-rouge">SecureRandom</code> latches onto the entropy source of the operating system, and reads the random bytes from that source (it is a file, usually). Therefore, <code class="language-plaintext highlighter-rouge">SecureRandom</code> is not deterministic and not repeatable. <code class="language-plaintext highlighter-rouge">Random</code>, however, is - which is where it becomes useful.</p>

<p>It is an implementation of something known as the <a href="https://en.wikipedia.org/wiki/Mersenne_Twister">Mersenne twister</a> and is built to produce a sequence of pseudorandom values which is achieved by various techniques of bit mixing and twiddling.</p>

<h2 id="a-random-is-stateful">A Random is stateful</h2>

<p>It is, after all, a <em>sequence.</em> A seeded <code class="language-plaintext highlighter-rouge">Random</code> object is able to produce pseudo-random numbers (or output) indefinitely, by revolving its internal byte bag - starting at the seed. You can think of it as an infinite <code class="language-plaintext highlighter-rouge">Range</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">counter</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="o">..</span><span class="p">).</span><span class="nf">each</span>
<span class="n">counter</span><span class="p">.</span><span class="nf">next</span> <span class="c1"># First call returns 0, second - 1, third - 2 and so forth</span>
</code></pre></div></div>

<p>except that the values will be wildly different and uniformly distributed, and will be also appropriately scaled. For example, if we want to obtain random numbers in the range from 0 to 0xFFFF we can create the same <code class="language-plaintext highlighter-rouge">Enumerator</code> with a Random:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">emitter</span> <span class="o">=</span> <span class="no">Enumerator</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">y</span><span class="o">|</span>
  <span class="n">rng</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
  <span class="kp">loop</span> <span class="p">{</span> <span class="n">y</span><span class="p">.</span><span class="nf">yield</span><span class="p">(</span><span class="n">rng</span><span class="p">.</span><span class="nf">rand</span><span class="p">(</span><span class="mh">0xFFFF</span><span class="p">))</span> <span class="p">}</span>
<span class="k">end</span>
<span class="n">emitter</span><span class="p">.</span><span class="nf">next</span> <span class="c1"># =&gt; 56422</span>
<span class="n">emitter</span><span class="p">.</span><span class="nf">next</span> <span class="c1"># =&gt; 15795</span>
</code></pre></div></div>

<p>Try it - it’s going to generate <em>exactly the same values</em> on your machine as it does on mine. So the first important trait of <code class="language-plaintext highlighter-rouge">Random</code> is that it is <strong>reproducible and deterministic.</strong> This in contrast to, for example, <code class="language-plaintext highlighter-rouge">Object#hash</code> in Ruby which is used for keying objects into hash tables - while the values of <code class="language-plaintext highlighter-rouge">hash</code> are unique, they are unique only across the virtual machine runtime - and they get generated afresh in every environment.</p>

<p>Another important trait of a <code class="language-plaintext highlighter-rouge">Random</code> is that it is <strong>stateful.</strong> When you make <code class="language-plaintext highlighter-rouge">Random</code> emit you data, that state changes irreversibly, and it only ever changes “forward” - like a ratchet. The state mutations (twists) depend on how much data is requested from <code class="language-plaintext highlighter-rouge">Random</code> in sequence. For example:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rng</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
<span class="n">rng</span><span class="p">.</span><span class="nf">rand</span><span class="p">(</span><span class="mh">0xFF</span><span class="p">)</span>
<span class="n">rng</span><span class="p">.</span><span class="nf">rand</span><span class="p">(</span><span class="mh">0xFFFF</span><span class="p">)</span> <span class="c1"># =&gt; 15795</span>

<span class="n">rng</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
<span class="n">rng</span><span class="p">.</span><span class="nf">rand</span><span class="p">(</span><span class="mh">0xFFFF</span><span class="p">)</span>
<span class="n">rng</span><span class="p">.</span><span class="nf">rand</span><span class="p">(</span><span class="mh">0xFFFF</span><span class="p">)</span> <span class="c1"># =&gt; 15795, same value</span>
</code></pre></div></div>

<p>but when we first request a “sizeable” blob of bytes</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rng</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
<span class="n">rng</span><span class="p">.</span><span class="nf">bytes</span><span class="p">(</span><span class="mi">1024</span><span class="o">*</span><span class="mi">1024</span><span class="p">);</span> <span class="kp">nil</span>
<span class="n">rng</span><span class="p">.</span><span class="nf">rand</span><span class="p">(</span><span class="mh">0xFFFF</span><span class="p">)</span> <span class="c1"># =&gt; 2321, different</span>
</code></pre></div></div>

<p>So while <code class="language-plaintext highlighter-rouge">Random</code> sequences are repeatable, they are not guaranteed to produce the same sequences of values if different amount of generated entropy is requested. The number of “twists” a <code class="language-plaintext highlighter-rouge">Random</code> will have gone through by the time you request data from it depends on the number of “twists” so far performend - and on the amount generated “entropy” emitted so far.</p>

<h2 id="when-fast-is-better-than-secure">When “fast” is better than “secure”</h2>

<p><code class="language-plaintext highlighter-rouge">Random</code> is also much, much faster than <code class="language-plaintext highlighter-rouge">SecureRandom</code>. For example, if you want to generate some random bags of bytes:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"securerandom"</span>  <span class="c1"># This adds uuid methods to Random</span>

<span class="k">def</span> <span class="nf">timed</span><span class="p">(</span><span class="n">what</span><span class="p">)</span>
  <span class="n">t</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_MONOTONIC</span><span class="p">)</span>
  <span class="k">yield</span>
  <span class="n">dt</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_MONOTONIC</span><span class="p">)</span> <span class="o">-</span> <span class="n">t</span>
  <span class="nb">puts</span><span class="p">(</span><span class="s2">"%s - %0.6f seconds"</span> <span class="o">%</span> <span class="p">[</span><span class="n">what</span><span class="p">,</span> <span class="n">dt</span><span class="p">])</span>
<span class="k">end</span>

<span class="n">timed</span><span class="p">(</span><span class="s2">"SecureRandom"</span><span class="p">)</span> <span class="k">do</span>
  <span class="mi">10_000</span><span class="p">.</span><span class="nf">times</span> <span class="p">{</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">uuid</span> <span class="p">}</span>
<span class="k">end</span>

<span class="n">timed</span><span class="p">(</span><span class="s2">"Random"</span><span class="p">)</span> <span class="k">do</span>
  <span class="mi">10_000</span><span class="p">.</span><span class="nf">times</span> <span class="p">{</span> <span class="no">Random</span><span class="p">.</span><span class="nf">uuid</span> <span class="p">}</span>
<span class="k">end</span>

<span class="n">timed</span><span class="p">(</span><span class="s2">"Random obj"</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">rng</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span>
  <span class="mi">10_000</span><span class="p">.</span><span class="nf">times</span> <span class="p">{</span> <span class="n">rng</span><span class="p">.</span><span class="nf">uuid</span> <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>gives:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SecureRandom - 0.025562 seconds
Random - 0.018445 seconds
Random obj - 0.010401 seconds
</code></pre></div></div>

<p>Using a pre-seeded <code class="language-plaintext highlighter-rouge">Random</code> is 2.5x faster than using SecureRandom. Moreover, in many situations it will be more appropriate too! Depending on the speed of your system, using <code class="language-plaintext highlighter-rouge">Random#bytes</code> can be much faster than <code class="language-plaintext highlighter-rouge">SecureRandom.bytes</code> as well.</p>

<p>Note that requiring <code class="language-plaintext highlighter-rouge">securerandom</code> adds several compatibility methods to <code class="language-plaintext highlighter-rouge">Random</code> (like <code class="language-plaintext highlighter-rouge">uuid</code>, <code class="language-plaintext highlighter-rouge">hex</code>, <code class="language-plaintext highlighter-rouge">base64</code>, etc.) that make it more interchangeable with <code class="language-plaintext highlighter-rouge">SecureRandom</code> while maintaining the deterministic behavior of <code class="language-plaintext highlighter-rouge">Random</code>.</p>

<h2 id="where-can-you-use-a-random">Where can you use a Random?</h2>

<p>A couple methods in Ruby stdlib actually accept a <code class="language-plaintext highlighter-rouge">random:</code>, and the argument can be either an instance of <code class="language-plaintext highlighter-rouge">Random</code> or <code class="language-plaintext highlighter-rouge">SecureRandom</code> (the module). Those are <code class="language-plaintext highlighter-rouge">Array#sample</code> and <code class="language-plaintext highlighter-rouge">Array#shuffle</code> and they are mighty useful:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="mi">0</span><span class="o">..</span><span class="mi">999</span><span class="p">).</span><span class="nf">to_a</span><span class="p">.</span><span class="nf">sample</span><span class="p">(</span><span class="ss">random: </span><span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">42</span><span class="p">))</span> <span class="c1">#=&gt; 102</span>
<span class="p">(</span><span class="mi">0</span><span class="o">..</span><span class="mi">999</span><span class="p">).</span><span class="nf">to_a</span><span class="p">.</span><span class="nf">sample</span><span class="p">(</span><span class="ss">random: </span><span class="no">SecureRandom</span><span class="p">)</span> <span class="c1">#=&gt; something truly random</span>
</code></pre></div></div>

<p>On its own you use it to obtain a random value from 0 to 1.0:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rng</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">12</span><span class="p">)</span>
<span class="n">rng</span><span class="p">.</span><span class="nf">rand</span> <span class="c1">#=&gt; # 0.3745401188473625</span>
</code></pre></div></div>

<p>which is useful for sampling a uniform distribution (if something ends up below a threshold value - it gets picked, otherwise - not).</p>

<h2 id="seeds-seeds-everywhere">Seeds, seeds everywhere</h2>

<p>When you have flaky tests, usually you will look for the <code class="language-plaintext highlighter-rouge">--seed=872771</code> printed to the console, and you try to reproduce your failure with that <code class="language-plaintext highlighter-rouge">--seed</code> value. What does it do?</p>

<p>Well, when tests are order-independent, most likely it does something like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">all_tests</span> <span class="o">=</span> <span class="n">collect_test_cases</span><span class="p">.</span><span class="nf">sort_by</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:name</span><span class="p">)</span>
<span class="n">all_tests</span><span class="p">.</span><span class="nf">shuffle!</span><span class="p">(</span><span class="ss">random: </span><span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">seed_arg</span><span class="p">))</span>
</code></pre></div></div>

<p>This <em>deterministically</em> shuffles the tests so that they run in random order, and will shuffle them in the same order every time. This seed is actually available inside of your tests, and it is an absolute requirement to make your tests reliable. For example, imagine you are testing some kind of event processing, and you need to test events arriving out of order. In your test, you may do</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">test_does_not_raise_when_events_arrive_out_of_order</span>
  <span class="n">assert_nothing_raised</span> <span class="k">do</span>
    <span class="n">events</span> <span class="o">=</span> <span class="p">[</span><span class="ss">:payment_received</span><span class="p">,</span> <span class="ss">:payment_chargeback</span><span class="p">,</span> <span class="ss">:payment_adjustment</span><span class="p">]</span>
    <span class="n">events</span><span class="p">.</span><span class="nf">shuffle!</span>
    <span class="n">event_processor</span><span class="p">.</span><span class="nf">process_event_stream</span><span class="p">(</span><span class="n">events</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>You now have a flaky test - you can’t say what the ordering of events under test is going to be, and if your <code class="language-plaintext highlighter-rouge">process_event_stream</code> does, indeed, raise - you won’t be able to quickly reproduce the same ordering that caused the failure. And there are 9 possible orderings here! The fix?</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">events</span><span class="p">.</span><span class="nf">shuffle!</span><span class="p">(</span><span class="ss">random: </span><span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="no">Minitest</span><span class="p">.</span><span class="nf">seed</span><span class="p">))</span>
</code></pre></div></div>

<p>This feeds <code class="language-plaintext highlighter-rouge">Array#shuffle!</code> a deterministic random number sequence generator which is guaranteed to produce the same <em>sequence</em> of values given the same seed. And if your ordering comes out a particular way with <code class="language-plaintext highlighter-rouge">--seed=123</code> you will be able to reproduce your failing test, 100% accurately. The same facility is available in RSpec under <code class="language-plaintext highlighter-rouge">RSpec.configuration.seed</code> by the way.</p>

<p>That predictable randomness goes further. For example, imagine you are testing something like ActiveStorage and need to ensure that your checksum calculation routine somewhere far down in your library stores your data correctly. You can, of course, have a fixture like <code class="language-plaintext highlighter-rouge">image.png</code> in your <code class="language-plaintext highlighter-rouge">fixtures/files/</code> and “fake-upload” it inside of your test. But a much faster way - which is also not going to have an adverse effect on your checkout speeds, would be:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rng</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="no">Minitest</span><span class="p">.</span><span class="nf">seed</span><span class="p">)</span>
<span class="n">key</span> <span class="o">=</span> <span class="n">rng</span><span class="p">.</span><span class="nf">hex</span><span class="p">(</span><span class="mi">12</span><span class="p">)</span>
<span class="n">random_binary_data</span> <span class="o">=</span> <span class="n">rng</span><span class="p">.</span><span class="nf">bytes</span><span class="p">(</span><span class="mi">33</span><span class="o">*</span><span class="mi">1024</span><span class="p">)</span>
<span class="n">sha2</span> <span class="o">=</span> <span class="no">Digest</span><span class="o">::</span><span class="no">SHA2</span><span class="p">.</span><span class="nf">hexdigest</span><span class="p">(</span><span class="n">random_binary_data</span><span class="p">)</span>

<span class="n">uploader</span><span class="p">.</span><span class="nf">upload</span><span class="p">(</span><span class="ss">key: </span><span class="n">key</span><span class="p">,</span> <span class="ss">io: </span><span class="no">StringIO</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">random_binary_data</span><span class="p">))</span>
<span class="n">blob</span> <span class="o">=</span> <span class="no">StoredBlob</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">key: </span><span class="n">key</span><span class="p">).</span><span class="nf">first!</span>
<span class="n">assert_equal</span> <span class="n">sha2</span><span class="p">,</span> <span class="n">blob</span><span class="p">.</span><span class="nf">sha2</span>
</code></pre></div></div>

<p>A call to <code class="language-plaintext highlighter-rouge">rand</code> without arguments gives you a random float between <code class="language-plaintext highlighter-rouge">0.0</code> and <code class="language-plaintext highlighter-rouge">1.0</code> If you have a feature flag which is deployed to 20% of your userbase, you can find out whether your user should receive a feature or not:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rng</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span>
<span class="n">rng</span><span class="p">.</span><span class="nf">rand</span> <span class="o">&lt;=</span> <span class="mf">0.2</span> <span class="c1"># =&gt; true or false</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Random</code> accepts a <em>seed</em> which is a large int. While <code class="language-plaintext highlighter-rouge">Random</code> doesn’t natively support string seeds, you can convert a string to an integer seed using this simple approach using bit shifting:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Convert string to a deterministic integer seed</span>
<span class="n">seed_value</span> <span class="o">=</span> <span class="n">seed_string</span><span class="p">.</span><span class="nf">bytes</span><span class="p">.</span><span class="nf">inject</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">acc</span><span class="p">,</span> <span class="n">byte</span><span class="o">|</span> <span class="p">(</span><span class="n">acc</span> <span class="o">&lt;&lt;</span> <span class="mi">8</span><span class="p">)</span> <span class="o">|</span> <span class="n">byte</span> <span class="p">}</span>
<span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">seed_value</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="fractal-sequencing-and-faker">Fractal sequencing and Faker</h2>

<p>Nothing lends itself better to <code class="language-plaintext highlighter-rouge">Random</code> application than generating all sorts of fake data. At <a href="https://cheddar.me">Cheddar</a> we had a fake bank, used for internal testing, where entire graphs of user data would be generated using <code class="language-plaintext highlighter-rouge">Random</code> combined with proper <code class="language-plaintext highlighter-rouge">Faker</code> use. This would work using seed sets. Imagine you need to generate a number of fake users, and every user has a certain number of fake accounts. These accounts, in turn, have transactions in them.</p>

<p>Since we know that a <code class="language-plaintext highlighter-rouge">Random</code> produces a deterministic sequence, we can use a derivation process to chain <code class="language-plaintext highlighter-rouge">Random</code> generators together. A single RNG would be initialized for the entire seed set. Then, for every user, we would make our “root” RNG generate a seed, which would be used to seed the RNG for the user. Inside the user we would get a seed from that RNG to generate the list of accounts, and inside of those accounts - transactions. Observe:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">with_faker_random</span><span class="p">(</span><span class="n">rng</span><span class="p">)</span>
  <span class="n">r</span> <span class="o">=</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Config</span><span class="p">.</span><span class="nf">random</span>
  <span class="no">Faker</span><span class="o">::</span><span class="no">Config</span><span class="p">.</span><span class="nf">random</span> <span class="o">=</span> <span class="n">rng</span>
  <span class="k">yield</span>
<span class="k">ensure</span>
  <span class="no">Faker</span><span class="o">::</span><span class="no">Config</span><span class="p">.</span><span class="nf">random</span> <span class="o">=</span> <span class="n">r</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">with_faker_seed</span><span class="p">(</span><span class="n">seed</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">blk</span><span class="p">)</span> <span class="o">=</span> <span class="n">with_faker_random</span><span class="p">(</span><span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">seed</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">blk</span><span class="p">)</span>

<span class="no">SEED_MAX</span> <span class="o">=</span> <span class="mh">0xFFFFFFFFFFFFFF</span> <span class="c1"># This is slightly more complicated but will do</span>

<span class="k">def</span> <span class="nf">generate_user</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
  <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span>
    <span class="ss">name: </span><span class="n">with_faker_seed</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span> <span class="p">{</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Name</span><span class="p">.</span><span class="nf">name</span> <span class="p">},</span>
    <span class="ss">email: </span><span class="n">with_faker_seed</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span> <span class="p">{</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Internet</span><span class="p">.</span><span class="nf">email</span> <span class="p">}</span>
  <span class="p">)</span>
  <span class="n">seed_for_accounts</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">seed</span><span class="p">).</span><span class="nf">rand</span><span class="p">(</span><span class="no">SEED_MAX</span><span class="p">)</span>
  <span class="n">generate_accounts</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">seed_for_accounts</span><span class="p">)</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">generate_accounts</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">accounts_seed</span><span class="p">)</span>
  <span class="n">rng</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">accounts_seed</span><span class="p">)</span>
  <span class="n">with_faker_random</span><span class="p">(</span><span class="n">rng</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">account</span> <span class="o">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">accounts</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">iban: </span><span class="no">Faker</span><span class="o">::</span><span class="no">Bank</span><span class="p">.</span><span class="nf">iban</span><span class="p">)</span>
    <span class="n">generate_transactions</span><span class="p">(</span><span class="n">account</span><span class="p">,</span> <span class="n">rng</span><span class="p">.</span><span class="nf">rand</span><span class="p">(</span><span class="no">SEED_MAX</span><span class="p">))</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">generate_transactions</span><span class="p">(</span><span class="n">account</span><span class="p">,</span> <span class="n">transactions_seed</span><span class="p">)</span>
  <span class="n">amount_range_cents</span> <span class="o">=</span> <span class="mi">1</span><span class="o">..</span><span class="mi">2500_00</span>
  <span class="n">time_range</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">utc</span><span class="p">(</span><span class="mi">2021</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">25</span><span class="p">)</span><span class="o">..</span><span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">.</span><span class="nf">utc</span>
  <span class="n">multipliers</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="c1"># For debit/credit</span>

  <span class="n">rng</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">transactions_seed</span><span class="p">)</span>
  <span class="n">n_txns</span> <span class="o">=</span> <span class="n">rng</span><span class="p">.</span><span class="nf">rand</span><span class="p">(</span><span class="mi">50</span><span class="o">..</span><span class="mi">3000</span><span class="p">)</span>
  <span class="n">n_txns</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span> <span class="o">|</span><span class="n">n</span><span class="o">|</span>
    <span class="n">amount</span> <span class="o">=</span> <span class="n">rng</span><span class="p">.</span><span class="nf">rand</span><span class="p">(</span><span class="n">amount_range_cents</span><span class="p">)</span> <span class="o">*</span> <span class="n">multipliers</span><span class="p">.</span><span class="nf">sample</span><span class="p">(</span><span class="ss">random: </span><span class="n">rng</span><span class="p">)</span>
    <span class="n">created_at</span> <span class="o">=</span> <span class="n">rng</span><span class="p">.</span><span class="nf">rand</span><span class="p">(</span><span class="n">time_range</span><span class="p">)</span>
    <span class="n">account</span><span class="p">.</span><span class="nf">transactions</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="n">amount</span><span class="p">:,</span> <span class="n">created_at</span><span class="p">:)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>If you read this closely you will see that the entire output graph (<code class="language-plaintext highlighter-rouge">User -&lt; Account(N) -&lt; Transaction(N)</code>) gets produced from a single random seed, percolating downwards.</p>

<p>There is one tricky bit here: the difference between <code class="language-plaintext highlighter-rouge">with_faker_random</code> and <code class="language-plaintext highlighter-rouge">with_faker_seed</code>. The <code class="language-plaintext highlighter-rouge">with_faker_seed</code> is useful when we need to output multiple pieces of data which should be deterministic regardless of order. Specifically, imagine we want our <code class="language-plaintext highlighter-rouge">User</code> to be generated with the same, predictable email and the same, predictable name. If we set the RNG for Faker and “leave it be”, the values generated depend on call order - which is to be expected, because Faker ratchets our RNG for obtaining entropy out of it, repeatedly - and the RNG is stateful:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">033</span><span class="o">&gt;</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Config</span><span class="p">.</span><span class="nf">random</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="c1">#&lt;Random:0x00000001395b3fa8&gt;</span>
<span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">034</span><span class="o">&gt;</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Name</span><span class="p">.</span><span class="nf">name_with_middle</span>
<span class="o">=&gt;</span> <span class="s2">"Brittany Klocko Prohaska"</span>
<span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">035</span><span class="o">&gt;</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Internet</span><span class="p">.</span><span class="nf">email</span>
<span class="o">=&gt;</span> <span class="s2">"lyndon_rempel@schumm-jaskolski.test"</span>
</code></pre></div></div>

<p>But if we do the calls in reverse, the output changes!</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">036</span><span class="o">&gt;</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Config</span><span class="p">.</span><span class="nf">random</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="c1">#&lt;Random:0x000000012ea9dfd8&gt;</span>
<span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">037</span><span class="o">&gt;</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Internet</span><span class="p">.</span><span class="nf">email</span>
<span class="o">=&gt;</span> <span class="s2">"ruby_ebert@hamill.test"</span>
<span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">03</span><span class="mi">8</span><span class="o">&gt;</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Name</span><span class="p">.</span><span class="nf">name_with_middle</span>
<span class="o">=&gt;</span> <span class="s2">"Zachery Weimann McGlynn"</span>
</code></pre></div></div>

<p>If that works for your use case - that’s fine, and it will be faster too! But if you want truly deterministic output, the correct way is to assign a “fresh” RNG before every Faker invocation:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">03</span><span class="mi">9</span><span class="o">&gt;</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Config</span><span class="p">.</span><span class="nf">random</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="c1">#&lt;Random:0x0000000129411458&gt;</span>
<span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">040</span><span class="o">&gt;</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Name</span><span class="p">.</span><span class="nf">name_with_middle</span>
<span class="o">=&gt;</span> <span class="s2">"Brittany Klocko Prohaska"</span>
<span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">041</span><span class="o">&gt;</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Config</span><span class="p">.</span><span class="nf">random</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="c1">#&lt;Random:0x00000001396155a0&gt;</span>
<span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">042</span><span class="o">&gt;</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Internet</span><span class="p">.</span><span class="nf">email</span>
<span class="o">=&gt;</span> <span class="s2">"ruby_ebert@hamill.test"</span>
</code></pre></div></div>

<p>It can be tricky to twist your head around this (pun intended!) but it can create fairly nice fractal graphs of objects.</p>

<h2 id="reproducing-distributions">Reproducing distributions</h2>

<p>Imagine you want to simulate some background jobs. Your APM gives you a distribution of the job run time. You know that the mean time for the job to complete is 433ms, but the p90 can go up to 1,56s of duration. You want to simulate queue load which is saturated with those jobs and you want them to have a similar profile. You can do that, and you can do it in a predictable way:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># This class takes the mean and the p90 (which will normally be durations of a task taken from Appsignal)</span>
<span class="c1"># and uses those to generate a random job duration. It should allow us to have reasonable simulated</span>
<span class="c1"># job durations. This is an https://en.wikipedia.org/wiki/Inverse_transform_sampling function.</span>
<span class="k">class</span> <span class="nc">Distribution</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">mean</span><span class="p">:,</span> <span class="n">p90</span><span class="p">:,</span> <span class="ss">random: </span><span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">)</span>
    <span class="vi">@p90</span> <span class="o">=</span> <span class="n">p90</span><span class="p">.</span><span class="nf">to_f</span>
    <span class="vi">@mean</span> <span class="o">=</span> <span class="n">mean</span><span class="p">.</span><span class="nf">to_f</span>
    <span class="vi">@std_dev</span> <span class="o">=</span> <span class="p">(</span><span class="vi">@p90</span> <span class="o">-</span> <span class="vi">@mean</span><span class="p">)</span> <span class="o">/</span> <span class="mf">1.28</span>
    <span class="vi">@random</span> <span class="o">=</span> <span class="n">random</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">value</span>
    <span class="c1"># Calculate the standard deviation using the provided mean and 90th percentile</span>
    <span class="n">u1</span> <span class="o">=</span> <span class="vi">@random</span><span class="p">.</span><span class="nf">rand</span>
    <span class="n">u2</span> <span class="o">=</span> <span class="vi">@random</span><span class="p">.</span><span class="nf">rand</span>
    <span class="n">r</span> <span class="o">=</span> <span class="no">Math</span><span class="p">.</span><span class="nf">sqrt</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span> <span class="o">*</span> <span class="no">Math</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="n">u1</span><span class="p">))</span>
    <span class="n">theta</span> <span class="o">=</span> <span class="mi">2</span> <span class="o">*</span> <span class="no">Math</span><span class="o">::</span><span class="no">PI</span> <span class="o">*</span> <span class="n">u2</span>
    <span class="n">z</span> <span class="o">=</span> <span class="n">r</span> <span class="o">*</span> <span class="no">Math</span><span class="p">.</span><span class="nf">cos</span><span class="p">(</span><span class="n">theta</span><span class="p">)</span>
    <span class="n">value</span> <span class="o">=</span> <span class="vi">@mean</span> <span class="o">+</span> <span class="vi">@std_dev</span> <span class="o">*</span> <span class="n">z</span>
    <span class="n">value</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="p">?</span> <span class="vi">@mean</span> <span class="o">-</span> <span class="n">value</span> <span class="p">:</span> <span class="n">value</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>and then, to create your job durations:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rng</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
<span class="n">dist</span> <span class="o">=</span> <span class="no">Distribution</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">mean: </span><span class="mf">433.0</span> <span class="o">/</span> <span class="mi">1000</span><span class="p">,</span> <span class="ss">p90: </span><span class="mf">1.56</span><span class="p">,</span> <span class="ss">random: </span><span class="n">rng</span><span class="p">)</span>
<span class="mi">2000</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span>
  <span class="n">simulator</span><span class="p">.</span><span class="nf">enqueue</span><span class="p">(</span><span class="no">SimulatedJob</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">duration: </span><span class="n">dist</span><span class="p">.</span><span class="nf">value</span><span class="p">))</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This and allows you to create very large datasets – which are perfect for performance testing. Need a few million bank transactions to test out a hypothesis? Grab your production distributions to have a plausible corpus, arm yourself with some <a href="/2025/01/maximum-speed-sqlite-inserts">wicked-fast SQLite INSERT mojo</a> and <code class="language-plaintext highlighter-rouge">Faker</code> and go to town, with a fully reproducible generated corpus.</p>

<h2 id="to-summarize">To summarize</h2>

<p>Mersenne twisters are awesome and <code class="language-plaintext highlighter-rouge">Random</code> is, in my view, the most undervalued Ruby class from the standard library. It can generate you mock data sets, it can drive entire graphs of output from Faker, and it can make your test suites super-reliable. And it is <em>wicked fast.</em></p>

<p>Also, it’s not really random - and I love cheating that way.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Sometimes, after a few pints in a respectable gathering of Rubyists, someone will ask me “what is the most undervalued module in the Ruby standard library?” There are many possible answers, of course, and some favoritism is to be expected. Piotr Szotkowski, who untimely passed away this summer, did a wonderful talk on the topic a wee while back. My personal answer to that question, however, would be Random. To me, Random is a unsung hero of a very large slice of the work we need to do in web applications, especially so when we need things to be deterministic and testable. So, let’s examine this little jewel a bit closer.]]></summary></entry><entry><title type="html">Actually doing things in user’s time zone</title><link href="https://blog.julik.nl/2025/10/chronically-actioned" rel="alternate" type="text/html" title="Actually doing things in user’s time zone" /><published>2025-10-01T00:00:00+00:00</published><updated>2025-10-01T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/10/chronically-actioned</id><content type="html" xml:base="https://blog.julik.nl/2025/10/chronically-actioned"><![CDATA[<p>My previous article about timezones turned out to be useful for quite a few folks, which makes me happy. <a href="https://www.azquotes.com/quote/654060">One candle lights another.</a></p>

<p>Ben Sheldon asked about then actually doing something with those converted times. How <em>do</em> you actually send a newsletter every morning on every working day, regardless of what the user’s time zone is?</p>

<p>There are a number of approaches to this - once you know the UTC time of the delivery. I will cover a few of them, including the one I prefer. Let’s wind the clocks!</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="approach-1-anything-can-be-done-in-postgres">Approach 1: Anything can be done in Postgres</h2>

<p>Remember how I told you that you should store your <code class="language-plaintext highlighter-rouge">time_zone</code> as an IANA identifier? There is a good reason for that. PostgreSQL natively supports timezone conversions. Observe:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- What was the time of that drip experiment in Moscow?</span>
<span class="k">SELECT</span> <span class="nb">TIMESTAMP</span> <span class="s1">'2011-11-25 18:00'</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'Europe/Moscow'</span> <span class="k">AS</span> <span class="n">time_at</span>
</code></pre></div></div>

<p>For me the return value is <code class="language-plaintext highlighter-rouge">2011-11-25 15:00:00+01</code>. The +01 UTC offset (which is what Postgres stores in the <code class="language-plaintext highlighter-rouge">timestamp with time zone</code> type) is because my laptop is currently in London, which is on DST and thus 1 hour ahead of UTC. The timezone setting of the box matters, thus. If we select the same time but a few years forward the conversion is also correct:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="nb">TIMESTAMP</span> <span class="s1">'2019-11-25 18:00'</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'Europe/Moscow'</span> <span class="k">AS</span> <span class="n">time_at</span>
</code></pre></div></div>

<p>gives <code class="language-plaintext highlighter-rouge">2019-11-25 16:00:00+01</code>. Now, if your database time is UTC (as it should be) and your application works with UTC internally (and it should!), we can tack another <code class="language-plaintext highlighter-rouge">AT TIME ZONE</code> into our <code class="language-plaintext highlighter-rouge">SELECT</code> to find out what time it was in UTC:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="p">(</span><span class="nb">TIMESTAMP</span> <span class="s1">'2011-11-25 18:00'</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'Europe/Moscow'</span><span class="p">)</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'UTC'</span> <span class="k">AS</span> <span class="n">time_at_utc</span>
</code></pre></div></div>

<p>which gives <code class="language-plaintext highlighter-rouge">2011-11-25 14:00:00</code> and no UTC offset. This is the time we <em>know now</em> the event took place at, and it is guaranteed to be correct. Now, let’s imagine we have a few users, and they have their timezone settings, and they all want to receive the newsletter at a certain time:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">WITH</span> <span class="n">accounts</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="p">(</span>
  <span class="k">VALUES</span>
     <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="s1">'Asia/Kolkata'</span><span class="p">,</span> <span class="s1">'9:10'</span><span class="p">),</span>
     <span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="s1">'America/New_York'</span><span class="p">,</span> <span class="s1">'9:00'</span><span class="p">),</span>
     <span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="s1">'Europe/Amsterdam'</span><span class="p">,</span> <span class="s1">'9:15'</span><span class="p">)</span>
    <span class="p">)</span> <span class="k">AS</span> <span class="n">a</span> <span class="p">(</span><span class="n">id</span><span class="p">,</span> <span class="n">time_zone</span><span class="p">,</span> <span class="n">delivery_time</span><span class="p">)</span>
<span class="p">)</span> <span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">accounts</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>id</th>
      <th>time_zone</th>
      <th>delivery_time</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Asia/Kolkata</td>
      <td>9:10</td>
    </tr>
    <tr>
      <td>2</td>
      <td>America/New_York</td>
      <td>9:00</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Europe/Amsterdam</td>
      <td>9:15</td>
    </tr>
  </tbody>
</table>

<p>I am specifically picking Kolkata as one of the timezones to test as it has a peculiar UTC offset - not in whole hours but whole hours and 30 minutes. We can then do a bit of expansion on that and select the 2 next delivery times as UTC (one for today’s date and one for the date after), and then pick the one which is not behind <code class="language-plaintext highlighter-rouge">NOW()</code> or - if both are ahead - just the first one (LLMs may or may not have been involved in writing this):</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">WITH</span> <span class="n">accounts</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="p">(</span>
    <span class="k">VALUES</span>
     <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="s1">'Asia/Kolkata'</span><span class="p">,</span> <span class="s1">'9:10'</span><span class="p">),</span>
     <span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="s1">'America/New_York'</span><span class="p">,</span> <span class="s1">'9:00'</span><span class="p">),</span>
     <span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="s1">'Europe/Amsterdam'</span><span class="p">,</span> <span class="s1">'9:15'</span><span class="p">)</span>
    <span class="p">)</span> <span class="k">AS</span> <span class="n">a</span> <span class="p">(</span><span class="n">id</span><span class="p">,</span> <span class="n">time_zone</span><span class="p">,</span> <span class="n">delivery_time</span><span class="p">)</span>
<span class="p">),</span>
<span class="n">next_delivery_times</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span> 
    <span class="n">id</span><span class="p">,</span>
    <span class="n">time_zone</span><span class="p">,</span>
    <span class="n">delivery_time</span><span class="p">,</span>
    <span class="c1">-- Create today's date with the delivery time in the user's timezone, then convert to UTC</span>
    <span class="p">(</span><span class="k">CURRENT_DATE</span> <span class="o">+</span> <span class="n">delivery_time</span><span class="p">::</span><span class="nb">time</span><span class="p">)</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="n">time_zone</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'UTC'</span> <span class="k">AS</span> <span class="n">today_utc</span><span class="p">,</span>
    <span class="c1">-- Create tomorrow's date with the delivery time in the user's timezone, then convert to UTC</span>
    <span class="p">(</span><span class="k">CURRENT_DATE</span> <span class="o">+</span> <span class="n">INTERVAL</span> <span class="s1">'1 day'</span> <span class="o">+</span> <span class="n">delivery_time</span><span class="p">::</span><span class="nb">time</span><span class="p">)</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="n">time_zone</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'UTC'</span> <span class="k">AS</span> <span class="n">tomorrow_utc</span><span class="p">,</span>
    <span class="c1">-- Current UTC time</span>
    <span class="n">NOW</span><span class="p">()</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'UTC'</span> <span class="k">AS</span> <span class="n">current_utc</span>
  <span class="k">FROM</span> <span class="n">accounts</span>
<span class="p">)</span>
<span class="k">SELECT</span> 
  <span class="n">id</span><span class="p">,</span>
  <span class="n">time_zone</span><span class="p">,</span>
  <span class="n">delivery_time</span><span class="p">,</span>
  <span class="k">CASE</span> 
    <span class="k">WHEN</span> <span class="n">today_utc</span> <span class="o">&gt;</span> <span class="n">current_utc</span> <span class="k">THEN</span> <span class="n">today_utc</span>
    <span class="k">ELSE</span> <span class="n">tomorrow_utc</span>
  <span class="k">END</span> <span class="k">AS</span> <span class="n">next_delivery_at</span>
<span class="k">FROM</span> <span class="n">next_delivery_times</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">next_delivery_at</span>
</code></pre></div></div>

<p>which gives</p>

<table>
  <thead>
    <tr>
      <th>id</th>
      <th>time_zone</th>
      <th>delivery_time</th>
      <th>next_delivery_at</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Asia/Kolkata</td>
      <td>9:10</td>
      <td>2025-10-02 03:40:00 UTC</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Europe/Amsterdam</td>
      <td>9:15</td>
      <td>2025-10-02 07:15:00 UTC</td>
    </tr>
    <tr>
      <td>2</td>
      <td>America/New_York</td>
      <td>9:00</td>
      <td>2025-10-02 13:00:00 UTC</td>
    </tr>
  </tbody>
</table>

<p>From there, we can enqueue our job to deliver the newsletters. This needs to be run repeatedly. Postgres will even be “smart” enough to adjust the time returned if the chosen time is in a DST transition. For example, during the transition to DST in a given timezone a time like 01:30 won’t exist because the clocks go one hour forward at 01:00 - so Postgres will helpfully adjust the value for you. Neat and it is what you want almost always.</p>

<p>⚠️ Do note that how you get <code class="language-plaintext highlighter-rouge">next_delivery_at</code> depends on the Postgres configuration. On some installs (and via Blazer) I could not force Postgres to return me UTC timestamps - they would always get displayed in the timezone of the Postgres server and session. But rest assured - they are correct, just shown with that stupid offset. If you want to make your life easier - use UTC for your database and thank me later.</p>

<h2 id="approach-2-our-fugit-method-from-before">Approach 2: Our Fugit method from before</h2>

<p>Essentially we do what we used to do via Postgres but in Rails. It will be substantially slower but can do the job. It is also likely to be much more readable for others on the team, and allows stuffing multiple events into one pattern (multiple times and multiple days of the week):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Account</span><span class="p">.</span><span class="nf">find_each</span> <span class="k">do</span> <span class="o">|</span><span class="n">account</span><span class="o">|</span>
  <span class="n">pattern</span> <span class="o">=</span> <span class="s2">"</span><span class="si">#{</span><span class="n">account</span><span class="p">.</span><span class="nf">delivery_time</span><span class="si">}</span><span class="s2"> in </span><span class="si">#{</span><span class="n">account</span><span class="p">.</span><span class="nf">time_zone</span><span class="p">.</span><span class="nf">name</span><span class="si">}</span><span class="s2">"</span>
  <span class="n">cron</span> <span class="o">=</span> <span class="no">Fugit</span><span class="p">.</span><span class="nf">do_parse_cronish</span><span class="p">(</span><span class="n">pattern</span><span class="p">)</span>
  <span class="n">occurs_at_utc</span> <span class="o">=</span> <span class="n">cron</span><span class="p">.</span><span class="nf">next_time</span><span class="p">(</span><span class="n">_reference</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">current</span><span class="p">).</span><span class="nf">utc</span>
  <span class="no">DeliverNewsletterJob</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="ss">wait_until: </span><span class="n">occurs_at_utc</span><span class="p">).</span><span class="nf">perform_later</span><span class="p">(</span><span class="n">account</span><span class="p">:)</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="detour-you-need-some-record">Detour: You need some record</h2>

<p>There is a caveat though. For example, imagine we did find the <code class="language-plaintext highlighter-rouge">next_delivery_at</code> this way, and we enqueue a delivery for account 1 to happen at 03:40 tomorrow. We do it like so:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">DeliverNewsletterJob</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="ss">wait_until: </span><span class="n">next_delivery_row</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s2">"next_delivery_at"</span><span class="p">)).</span><span class="nf">perform_later</span><span class="p">(</span><span class="ss">account: </span><span class="no">Account</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">next_delivery_row</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s2">"id"</span><span class="p">)))</span>
</code></pre></div></div>

<p>We now have a job in our queue, but - crucially - we can’t make sure we have just one! ActiveJobs do not have a good concept of <em>identity</em> so if the user goes into their settings and changes their delivery time to 8:00, and our “bulk-enqueue newsletters” task runs in 30 minutes, it will enqueue <em>another</em> <code class="language-plaintext highlighter-rouge">DeliverNewsletterJob</code> which will run earlier. This will mean that the user will receive 2 newsletters, with 1 hour and 10 minutes between them. Clearly not what we want.</p>

<p>Same if the user changes their delivery time to be later. Our job is already on the queue, so another one is going to be enqueued - and they, again, are going to receive two newsletters.</p>

<p>We could say “let’s just add a <code class="language-plaintext highlighter-rouge">newsletter_last_delivered_at</code> column onto <code class="language-plaintext highlighter-rouge">users</code> and record when we deliver, and skip if we did so recently”. This can work:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">WITH</span> <span class="n">accounts</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="p">(</span>
    <span class="k">VALUES</span>
     <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="s1">'Asia/Kolkata'</span><span class="p">,</span> <span class="s1">'9:10'</span><span class="p">,</span> <span class="p">(</span><span class="n">NOW</span><span class="p">()</span> <span class="o">-</span> <span class="s1">'2 hours'</span><span class="p">::</span><span class="n">interval</span><span class="p">)</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'UTC'</span><span class="p">),</span>
     <span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="s1">'America/New_York'</span><span class="p">,</span> <span class="s1">'9:00'</span><span class="p">,</span> <span class="k">NULL</span><span class="p">),</span>
     <span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="s1">'Europe/Amsterdam'</span><span class="p">,</span> <span class="s1">'9:15'</span><span class="p">,</span> <span class="k">NULL</span><span class="p">)</span>
    <span class="p">)</span> <span class="k">AS</span> <span class="n">a</span> <span class="p">(</span><span class="n">id</span><span class="p">,</span> <span class="n">time_zone</span><span class="p">,</span> <span class="n">delivery_time</span><span class="p">,</span> <span class="n">last_delivered_at</span><span class="p">)</span>
<span class="p">),</span>
<span class="n">next_delivery_times</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span> 
    <span class="n">id</span><span class="p">,</span>
    <span class="n">time_zone</span><span class="p">,</span>
    <span class="n">delivery_time</span><span class="p">,</span>
    <span class="n">last_delivered_at</span><span class="p">,</span>
    <span class="c1">-- Create today's date with the delivery time in the user's timezone, then convert to UTC</span>
    <span class="p">(</span><span class="k">CURRENT_DATE</span> <span class="o">+</span> <span class="n">delivery_time</span><span class="p">::</span><span class="nb">time</span><span class="p">)</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="n">time_zone</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'UTC'</span> <span class="k">AS</span> <span class="n">today_utc</span><span class="p">,</span>
    <span class="c1">-- Create tomorrow's date with the delivery time in the user's timezone, then convert to UTC</span>
    <span class="p">(</span><span class="k">CURRENT_DATE</span> <span class="o">+</span> <span class="n">INTERVAL</span> <span class="s1">'1 day'</span> <span class="o">+</span> <span class="n">delivery_time</span><span class="p">::</span><span class="nb">time</span><span class="p">)</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="n">time_zone</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'UTC'</span> <span class="k">AS</span> <span class="n">tomorrow_utc</span><span class="p">,</span>
    <span class="c1">-- Current UTC time</span>
    <span class="n">NOW</span><span class="p">()</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'UTC'</span> <span class="k">AS</span> <span class="n">current_utc</span>
  <span class="k">FROM</span> <span class="n">accounts</span>
<span class="p">),</span> <span class="n">next_delivery_per_account</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span> 
  <span class="n">id</span><span class="p">,</span>
  <span class="n">time_zone</span><span class="p">,</span>
  <span class="n">delivery_time</span><span class="p">,</span>
  <span class="n">last_delivered_at</span><span class="p">,</span>
  <span class="k">CASE</span> 
    <span class="k">WHEN</span> <span class="n">today_utc</span> <span class="o">&gt;</span> <span class="n">current_utc</span> <span class="k">THEN</span> <span class="n">today_utc</span>
    <span class="k">ELSE</span> <span class="n">tomorrow_utc</span>
  <span class="k">END</span> <span class="k">AS</span> <span class="n">next_delivery_at</span>
  <span class="k">FROM</span> <span class="n">next_delivery_times</span>
<span class="p">)</span> <span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">next_delivery_per_account</span> <span class="k">AS</span> <span class="n">a1</span>
<span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">accounts</span> <span class="k">AS</span> <span class="n">a2</span> <span class="k">ON</span> <span class="n">a2</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">a1</span><span class="p">.</span><span class="n">id</span> <span class="k">AND</span> <span class="p">((</span><span class="n">a1</span><span class="p">.</span><span class="n">next_delivery_at</span> <span class="o">-</span> <span class="n">a2</span><span class="p">.</span><span class="n">last_delivered_at</span> <span class="o">&gt;</span> <span class="s1">'24 hours'</span><span class="p">::</span><span class="n">interval</span><span class="p">)</span> <span class="k">OR</span> <span class="n">a2</span><span class="p">.</span><span class="n">last_delivered_at</span> <span class="k">IS</span> <span class="k">NULL</span><span class="p">)</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">next_delivery_at</span>
</code></pre></div></div>

<p>and you will see that our user in Kolkata is now excluded from the selection. But we will <em>also</em> need to do this check inside of our delivery job, because our <code class="language-plaintext highlighter-rouge">next_delivery_at</code> gets calculated <em>at a point in time.</em> So:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">DeliverNewsletterJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">account</span><span class="p">:)</span>
    <span class="k">return</span> <span class="k">if</span> <span class="n">account</span><span class="p">.</span><span class="nf">last_delivered_at</span> <span class="o">&amp;&amp;</span> <span class="n">account</span><span class="p">.</span><span class="nf">last_delivered_at</span> <span class="o">&gt;</span> <span class="mi">24</span><span class="p">.</span><span class="nf">hours</span><span class="p">.</span><span class="nf">ago</span>
    <span class="c1">#.. do the delivery</span>
    <span class="n">account</span><span class="p">.</span><span class="nf">touch</span><span class="p">(</span><span class="ss">:last_delivered_at</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This is somewhat safe.</p>

<h2 id="approach-3-modeling-it">Approach 3: Modeling it</h2>

<p>We see that using the <code class="language-plaintext highlighter-rouge">last_delivered_at</code> creates annoying gymnastics of all sorts. And while showing Postgres prowess is good fun - and Postgres is a great tool - I tend to prefer SQLite lately, for a “nimbler” setup. But that’s not all.</p>

<p>We actually have 2 entities in our setup, even though the second entity does not have its own data model just yet. In addition to an <code class="language-plaintext highlighter-rouge">Account</code> we also have a <code class="language-plaintext highlighter-rouge">Newsletter</code>. A user is receiving multiple newsletters, in succession. If a user changes their preferred delivery time, they are changing it on their current “pending” newsletter and all the newsletters going forward. Instead of trying to record timestamps all over and do arithmetic, why don’t we actually represent the <code class="language-plaintext highlighter-rouge">Newsletter</code> as an actual model?</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Account</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">has_many</span> <span class="ss">:newsletters</span><span class="p">,</span> <span class="o">-&gt;</span> <span class="p">{</span> <span class="n">order</span><span class="p">(</span><span class="ss">created_at: :desc</span><span class="p">)</span> <span class="p">},</span> <span class="ss">dependent: :delete_all</span> <span class="k">do</span>
    <span class="k">def</span> <span class="nf">current</span>
      <span class="c1"># Create or find the current pending newsletter</span>
      <span class="n">create_or_find_by</span><span class="p">(</span><span class="ss">account_id: </span><span class="n">association_owner</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span> <span class="ss">state: </span><span class="s2">"pending"</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="n">after_save</span> <span class="ss">:update_current_newsletter_delivery_time</span>

  <span class="k">def</span> <span class="nf">deliver_next_newsletter_at</span>
    <span class="n">pattern</span> <span class="o">=</span> <span class="s2">"</span><span class="si">#{</span><span class="n">delivery_time</span><span class="si">}</span><span class="s2"> in </span><span class="si">#{</span><span class="n">time_zone</span><span class="p">.</span><span class="nf">name</span><span class="si">}</span><span class="s2">"</span>
    <span class="n">newsletter_delivery_cron</span> <span class="o">=</span> <span class="no">Fugit</span><span class="p">.</span><span class="nf">do_parse_cronish</span><span class="p">(</span><span class="n">pattern</span><span class="p">)</span>
    <span class="n">newsletter_delivery_cron</span><span class="p">.</span><span class="nf">next_time</span><span class="p">(</span><span class="n">_reference</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">current</span><span class="p">).</span><span class="nf">utc</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">update_current_newsletter_delivery_time</span>
    <span class="n">newsletters</span><span class="p">.</span><span class="nf">current</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">deliver_at: </span><span class="n">deliver_next_newsletter_at</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">Newsletter</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">enum</span> <span class="ss">:state</span><span class="p">,</span> <span class="ss">pending: </span><span class="s2">"pending"</span><span class="p">,</span> <span class="ss">delivering: </span><span class="s2">"delivering"</span><span class="p">,</span> <span class="ss">delivered: </span><span class="s2">"delivered"</span>
  <span class="n">belongs_to</span> <span class="ss">:account</span>
  <span class="n">before_save</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">idempotency_key</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">uuid</span> <span class="k">if</span> <span class="n">it</span><span class="p">.</span><span class="nf">pending?</span> <span class="p">}</span>
  <span class="n">after_save</span> <span class="p">{</span> <span class="no">NewsletterDeliveryJob</span><span class="p">.</span><span class="nf">perform_later</span><span class="p">(</span><span class="n">it</span><span class="p">,</span> <span class="n">it</span><span class="p">.</span><span class="nf">idempotency_key</span><span class="p">)</span> <span class="k">if</span> <span class="n">it</span><span class="p">.</span><span class="nf">pending?</span> <span class="p">}</span>

  <span class="k">def</span> <span class="nf">perform_delivery!</span><span class="p">(</span><span class="n">idempotency_key_at_enqueue</span><span class="p">)</span>
    <span class="c1"># Check whether the idempotency key is the same. If it is not,</span>
    <span class="c1"># the delivery time has been changed and the job calling us is stale</span>
    <span class="n">with_lock</span> <span class="k">do</span>
      <span class="k">return</span> <span class="k">unless</span> <span class="n">pending?</span>
      <span class="k">return</span> <span class="k">unless</span> <span class="n">idempotency_key</span> <span class="o">==</span> <span class="n">idempotency_key_at_enqueue</span> 

      <span class="c1"># Mark this newsletter as delivering</span>
      <span class="n">update!</span><span class="p">(</span><span class="ss">state: </span><span class="s2">"delivering"</span><span class="p">)</span>

      <span class="c1"># Immediately create the _next_ newsletter</span>
      <span class="n">next_at</span> <span class="o">=</span> <span class="n">account</span><span class="p">.</span><span class="nf">deliver_next_newsletter_at</span>
      <span class="n">account</span><span class="p">.</span><span class="nf">newsletters</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">deliver_at: </span><span class="n">next_at</span><span class="p">,</span> <span class="ss">state: </span><span class="s2">"pending"</span><span class="p">)</span>
    <span class="k">end</span>
    <span class="c1"># ...do the delivery</span>
    <span class="c1">#</span>
    <span class="c1"># and mark as delivered</span>
    <span class="n">with_lock</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">state: </span><span class="s2">"delivered"</span><span class="p">)</span> <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">NewsletterDeliveryJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">newsletter</span><span class="p">,</span> <span class="n">idempotency_key_at_enqueue</span><span class="p">)</span>
    <span class="n">newsletter</span><span class="p">.</span><span class="nf">perform_delivery!</span><span class="p">(</span><span class="n">idempotency_key_at_enqueue</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>There are a number of things to explain here. The idempotency key is needed because while there is just one current newsletter, a job may still be in the queue to process that newsletter with its previous <code class="language-plaintext highlighter-rouge">deliver_at</code> value. This will prevent the same newsletter delivering twice or delivering too early.</p>

<p>Another nice aspect of this setup is that a <code class="language-plaintext highlighter-rouge">Newsletter</code> may, in itself, have linked <code class="language-plaintext highlighter-rouge">Articles</code> - and you will be able to deliver a newsletter <em>just</em> with articles which have been added to it so far.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Newsletter</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="c1"># ...</span>
  <span class="n">has_and_belongs_to_many</span> <span class="ss">:articles</span><span class="p">,</span> <span class="o">-&gt;</span> <span class="p">{</span> <span class="n">order</span><span class="p">(</span><span class="ss">created_by: :desc</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">Article</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">after_create</span> <span class="k">do</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">connection</span><span class="p">.</span><span class="nf">execute</span> <span class="o">&lt;&lt;~</span><span class="no">SQL</span><span class="sh">
      INSERT INTO articles_newsletters (article_id, newsletter_id)
      SELECT </span><span class="si">#{</span><span class="n">it</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="sh">, newsletters.id
      FROM newsletters
      WHERE newsletters.state = 'pending'
</span><span class="no">    SQL</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Yet another: you will now have actual records of when your users have received their newsletters, which will be traceable back in time.</p>

<p>And last: there usually are subtle race conditions in systems like this (especially when ActiveRecord callbacks are involved), so tread lightly.</p>

<h2 id="to-summarize">To summarize</h2>

<p>“Figuring out when” is just one part of the equation - actually doing them is another. While it does move more into the “durable executions” territory it is not rocket science, just try not to do the same thing twice. And please, please, please configure your server, DB and application to use UTC.</p>

<p>But try to model things well.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[My previous article about timezones turned out to be useful for quite a few folks, which makes me happy. One candle lights another. Ben Sheldon asked about then actually doing something with those converted times. How do you actually send a newsletter every morning on every working day, regardless of what the user’s time zone is? There are a number of approaches to this - once you know the UTC time of the delivery. I will cover a few of them, including the one I prefer. Let’s wind the clocks!]]></summary></entry><entry><title type="html">The boss of it all</title><link href="https://blog.julik.nl/2025/09/the-boss-of-it-all" rel="alternate" type="text/html" title="The boss of it all" /><published>2025-09-28T00:00:00+00:00</published><updated>2025-09-28T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/09/the-boss-of-it-all</id><content type="html" xml:base="https://blog.julik.nl/2025/09/the-boss-of-it-all"><![CDATA[<p>The recent <a href="https://joel.drapper.me/p/rubygems-takeover/">Ruby Central tragedy</a> has me in shambles, honestly. It cuts deep at the very spot where I am feeling the most insecurity and the most disenfranchisement.</p>

<p>The crux of the issue is creative control. Writing software is a creative endeavor, and we are just now barely getting to the understanding that even though free software promises open source, it does not promise open governance or shared ownership. Something made by a person is their creation, and in the world of pervasive corporate grift and endless growth-at-any-cost it remains one of the few, and - to my view - purest - forms of being attached to what you produce. Having creative control and exercising it is the ultimate form of caring - something that has become a dangerous trait in today’s software organizations.</p>

<p>I’ve bumped into <a href="https://www.seuros.com/">Abdelkader Boudih</a> a couple of times online, and - frankly - was somewhat put off because I don’t like it when people become rude towards me - personally - too soon. You may have a heated debate with me and we can call each other names, but we have to share a dram of liquor first. But also - because I am biased against “rude by default” as it is something I grew up in.</p>

<p>Man, all I can say - I owe you an apology.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="the-directors-of-it-all">The directors of it all</h2>

<p>That said, his <a href="https://www.seuros.com/blog/rubygems-coup-when-parasites-take-the-host/">initial writeup on the Ruby Central governance</a> mentioning a string of Director-level positions on LinkedIn, nearly had me in tears. I don’t have nearly the amount of vitriol to dispense, and I don’t know Marty personally, and I can’t talk about this in the same words. But I know exactly the type he is writing about, and I know exactly where the bitterness comes from. For I tasted that poison.</p>

<p>It is the <a href="https://web.archive.org/web/20201101040313/https://www.spakhm.com/p/how-to-get-promoted">ink in the water.</a> The seemingly kind, thoughtful person in meetings who seems never edgy, never speaking ahead. Always a good listener, a good ear for gossip, and talented at “stakeholder management”.</p>

<p>It is also the person that builds glass ceilings above you. The “you are not quite ready for this role” person. The “team X has been having difficulties with your stance” person. And it has little to do with bureaucracy - not quite. In our profession, folks like this are great at executing managerial capture, and - at the same time - the guardians of the very system which makes most software scale-ups a toxic environment.</p>

<p>And - and I’ve seen it first hand too (and had to be in that role) - the person who takes root access away from all the “undesirables” in the moment of crisis.</p>

<p>And I don’t want to pretend I am not jealous here either - I know <a href="/2025/09/illegible-perception">perception management</a> but I haven’t mastered it.</p>

<h2 id="how-much-fear-can-we-handle">How much fear can we handle?</h2>

<p>We don’t talk about this, but we know exactly what is implied. And, sometimes, I feel compelled to be the person who <em>does</em> talk about it. You may not want to hire Abdelkader because he writes about this, and you may not want to hire me – because I talk about it. It doesn’t matter, because there is a modicum of sincerity and professional integrity that is fundamental to what we do - we <em>create and build</em> and we work in a <em>trade.</em></p>

<p>I believe strongly that effective leadership in our field requires hands-on experience with the work being managed. A person who doesn’t understand the <em>trade</em> has no business leading teams of practitioners. And I’ve seen this principle play out in practice. Moreover - I believe that you have to consistently build together with those you lead, until such time that it becomes impossible from the scheduling perspective (or your org grows too large to sustain it).</p>

<p>This isn’t just theoretical for me – I’m convinced that the combination of hands-on technical work and management brings the most success to small and medium-size organizations. So strongly, in fact, that this is exactly what I offer: <a href="/hire-me">technical leadership that stays grounded in the craft</a>. Because I’ve been in both trenches.</p>

<p>And I’ve seen things play out with “the directors of it all”.</p>

<p>A manager escalates a senior developer to HR over unfounded concerns about workplace conduct. The manager was previously brought in by that very developer to help with stakeholder management that had become challenging. The developer has quit in the end, because the concerns turned out to be false yet had them utterly belittled. It is not clear whether this was necessary if they wanted the developer to quit - it was clear that the developer would have quit of their own accord if asked to.</p>

<p>A director weighing in on a job candidate saying that “I’ve read their blog, and I doubt they will be a good fit for the team”. The blog contained articles with <em>just some opinions</em> on how teams in high-performing organizations should be run. From the very British position of that director, even daring to open your mouth on that issue was a sufficient background for rejecting the candidate. Of course they were not the hiring manager on the team the person interviewed for. Of course they got their way.</p>

<p>As much as I don’t like the divisive bitterness, the way Abdelkader has put it is absolutely, completely <em>on point:</em></p>

<blockquote>
  <p>Six months of advising, then boom—he’s running the place. Classic management coup: get close as an “advisor,” then execute the takeover.</p>
</blockquote>

<p>One of the best <a href="/2024/04/joke-accounts-are-a-bitter-necessity">satire pieces</a> on the topic is <a href="https://www.imdb.com/title/tt0469754/">Direktøren for det hele</a> - a movie about a fake CEO of an informatics company installed to facilitate the acquisition of the company by a competitor.</p>

<h2 id="you-can-get-paid-but-just-for-the-work-you-do-for-me">You can get paid but just for the work you do for me</h2>

<p>And - coincidentally - it is exactly where I am struggling the most. One of the underlying narratives in the <a href="https://joel.drapper.me/p/ruby-central-fact-check/">Bundler schism</a> is that <a href="https://andre.arko.net/">André</a> was trying to make good money from maintaining Bundler. Not “just” money - <em>good</em> money.</p>

<p>By contrast, DHH’s position has always been that <a href="https://world.hey.com/dhh/the-open-source-gift-exchange-2171e0f0">open source software is a gift</a> and is supposed to be transacted as such. Coupled to that is a long known policy of 37Signals/Basecamp of <a href="https://books.37signals.com/books/3/pages/14">not allowing moonlighting.</a> David’s answers on Omarchy are even more telling. Of course you can treat all the maintainership work as a gift if you <a href="https://x.com/dhh/status/1964773032339366006">have the optionality to do so:</a></p>

<blockquote>
  <p>I don’t need revenue. I’m already rich.</p>
</blockquote>

<p>And make no mistake - I get jealous AF reading this. And so do many others. I would also love to look at things this way. I would also love to be a co-owner of a business to such an extent that I would put my own OSS creation whole-heartedly under <a href="https://github.com/basecamp/omarchy">the Github organisation of the business</a> and know it won’t be “sunset”, taken away from me or abandoned. Or that one-or-another “director of it all” won’t put an unenforceable license on it, or that a team of appointed designers is suddenly going to impose their design tokens system on the UI of it.</p>

<h2 id="they-giveth-they-taketh-away">They giveth, they taketh away</h2>

<p>Without going into much detail, let’s just say that so far my experience has been the opposite. The impact was significant enough that I am now cautious about placing <em>any</em> open source projects I create under <em>any</em> organization that I don’t directly control. To put things in perspective, at one company alone my legacy consists of <a href="https://rubygems.org/profiles/wetransfer">30+ libraries.</a> Some of those were built by my colleagues, but the majority - mostly by me. I no longer have the ability to maintain, expand, improve or derive benefit from <em>any</em> of them.</p>

<p>All by organizational decisions made by leadership, with impressive professional profiles. They have built careers out of a range of achievements - one of which has, as it turns out, been “sunsetting” the work of builders like me.</p>

<p>Neither can I fork them except under a very restrictive set of provisions - because <a href="https://alexbeaver.com/hfoss-hippocratic/">another organizational decision adopted a problematic license company-wide.</a></p>

<p>That remains a significant disappointment.</p>

<h2 id="competition-is-for-losers">Competition is for losers</h2>

<p>So it’s little wonder that the party line of Ruby Central sponsorship was to start seeing <a href="https://spinel.coop/">Spinel</a> as a threat. How dare those people try to make money off of something that is supposed to be a gift, especially since they have <a href="https://www.zdnet.com/article/reports-say-one-third-of-basecamp-employees-exit-in-the-wake-of-new-company-etiquette/">refused to partake in the fruits of the Basecamp success?</a> It was their choice, after all: stay with the leader, work the great salaried position (and - don’t get me wrong - 37Signals offers an amazing package, especially if you are not in the US) - and deliver OSS as a gift in perpetuity. Or hold a great position at <a href="https://canadianexecutivehub.com/2024/11/29/how-shopifys-strategic-growth-revolutionized-e-commerce-in-canada/">the little green bag company</a> and exercise stewardship over the parts of Rails which are not exclusive 37Signals terrain. <em>And be happy about it.</em></p>

<p>This cuts deep into the modern irony of entrepreneurship. <a href="https://www.youtube.com/watch?v=4yohVh4qcas">Daniel Priestley</a> is frequently appearing on various UK podcasts boasting about “enabling entrepreneurship” and the virtues of succeeding independently. Only there is a flipside to this, which entrepreneurs are not so fond of discussing: they know what they are paying for, and they - naturally, by laws of simple economics - want to get as much of it as possible</p>

<p>When you build for them, the value is your time, your ideas and the products of your labor. It is not quite straightforward to “be an entrepreneur” if you want to do something by yourself, because any move you make can be seen as competitive against your employer. Should you decide to take risks, they should be all <em>your risks.</em> Quit your job (no severance, of course), live out of a hotel in Vietnam and try to <em>make it</em> – just don’t dare to try it while you are still on our payroll.</p>

<p>And if you are producing something that is given for free - make no mistake, we can take it away from you by a vote of the board, and there will be a battery of Directors of It All ready to action the decision.</p>

<h2 id="we-want-all-of-your-time-all-of-the-time">We want all of your time, all of the time</h2>

<p>Same for time. Time is non-replenishable, and the pay usually is for 40 hours (at least in these parts of the world). But that’s not enough for a true entrepreneur - they want to <a href="https://www.seangoedecke.com/overemployment/">own the hours they don’t pay for as well</a> - even though those hours are not in the contract. Having the builder control what they produce - even as a gift - is not a part of the deal, and <a href="https://knowyourmeme.com/memes/i-am-altering-the-deal">pray the deal won’t get altered any further.</a></p>

<p>So - yes, “everyone can be an entrepreneur” but “not as long as that everyone is getting any kind of money from <em>me</em>.” It is very tempting to say this has to do with “loyalty and retention”, but - once in a truly bitter mood - we know that it could as well be called differently.</p>

<p>The issue with that setup is that it effectively narrows the open paths into just a few possible branches:</p>

<ul>
  <li>Become one of the “directors of it all”. Have a good LinkedIn. Befriend the right people and make everyone understand that you are <em>not a problem,</em> and that you <em>know how the game gets played.</em> Play the game, nod in agreement, rest+vest.</li>
  <li>Become a principal engineer and figure out what craft occupation is still available as a hobby, without it encroaching on your “no conflict of interest” clauses. Make sure to pick the right big company for the act.</li>
  <li>Go fully indie, start your own business, do whatever you like. Preferably somewhere where health insurance is affordable and there is still social housing to go about.</li>
</ul>

<p>Any kind of combo arrangement, or arrangement where software can produce value even though it is not under a corporate purview, clashes with the desire of the current tech employers to get maximum return on their talent investment - and with their innate, primordial fear of competition. The only exception is likely the gig economy, where market equilibrium will assure the minimum viable compensation automatically.</p>

<p>Which brings us back to the control angle.</p>

<h2 id="you-make-it-and-you-should-own-it">You make it and you should own it</h2>

<p>The tragedy of open source anno 2025 is that it has empowered the takers but disenfranchised the givers. Creating a gift is not enough. You also have to be employed by the right party. You have to be good at marketing. You have to have other sources of income - <em>and</em> you have to be smart enough to not say anything stupid. And don’t you dare to try and live off of it.</p>

<p>I hope this situation will change in our lifetime and we can yet live off of software we create without selling the company to Microsoft or IBM and have not only the joy of gifting, but something more tangible.</p>

<h2 id="the-weight-of-creation">The weight of creation</h2>

<p>There is something profound about the moment when code first compiles, when a library you wrote becomes the foundation for someone else’s dream. Yet we have allowed this act to be commodified, stripped of meaning, reduced to lines of code that can be taken away by corporate decree.</p>

<p>This is the great lie of our time: that generosity must come with surrender, that the gift of code must be accompanied by the gift of control. Control is a double-edged sword—whether it’s a politically motivated moderation team or a corporate boss, both are eager to take it from the maker, each in the name of their own agenda.</p>

<p>But here is the truth: <strong>you made it, therefore you should own it.</strong> The libraries you create should remain under your care, your vision, your judgment of what serves the community best.</p>

<p>The entrepreneurs who have built empires on open source must learn to loosen their grip. The corporate open-source model has failed for most – it has created a landscape where the most valuable contributions are made by those who can least afford to make them.</p>

<p>We need a new covenant: one where maintainers and authors retain the right to shape their creations, not because they are selfish, but because they understand them best.</p>

<p>Own what you create. Not because you are greedy, but because you are responsible. The future of open source lies in the hands of those who actually build, not the “directors of it all.”</p>

<p>There is no barrier to building, and the only way to overcome bitterness is to create something great. Let’s strive for that.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[The recent Ruby Central tragedy has me in shambles, honestly. It cuts deep at the very spot where I am feeling the most insecurity and the most disenfranchisement. The crux of the issue is creative control. Writing software is a creative endeavor, and we are just now barely getting to the understanding that even though free software promises open source, it does not promise open governance or shared ownership. Something made by a person is their creation, and in the world of pervasive corporate grift and endless growth-at-any-cost it remains one of the few, and - to my view - purest - forms of being attached to what you produce. Having creative control and exercising it is the ultimate form of caring - something that has become a dangerous trait in today’s software organizations. I’ve bumped into Abdelkader Boudih a couple of times online, and - frankly - was somewhat put off because I don’t like it when people become rude towards me - personally - too soon. You may have a heated debate with me and we can call each other names, but we have to share a dram of liquor first. But also - because I am biased against “rude by default” as it is something I grew up in. Man, all I can say - I owe you an apology.]]></summary></entry><entry><title type="html">Scheduling things in user’s time zone</title><link href="https://blog.julik.nl/2025/09/chronically-regular" rel="alternate" type="text/html" title="Scheduling things in user’s time zone" /><published>2025-09-26T00:00:00+00:00</published><updated>2025-09-26T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/09/chronically-regular</id><content type="html" xml:base="https://blog.julik.nl/2025/09/chronically-regular"><![CDATA[<p>Doing something at a time convenient for the user is a recurring (sic!) challenge with web applications. And the more users you have across a multitude of time zones, the more pressing it becomes to do it well.</p>

<p>It is actually not that hard, but it does have a few fiddly bits which can be challenging to put together. So, let’s do some time traveling.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="what-makes-time-zones-so-tricky">What makes time zones so tricky?</h2>

<p>Time zones are tricky because they <em>change over time.</em> There is a global UTC clock, which has <a href="https://en.wikipedia.org/wiki/Leap_second">leap seconds</a> – that’s already a bit tricky, but not that tricky. Timezones are tricky because they are <a href="/2025/07/data-over-time">data over time,</a> and it matters not only what time you need to convert from local to UTC and back, but also <em>from</em> what time. Let me explain.</p>

<p>Imagine you are a person in 2010 and you are planning to go to an event in Moscow, and the organizers have told you the event is going to take place at 18:00 on the 25th of November 2011. You put the event in your calendar, setting its time and date.</p>

<p>Meanwhile, a newly-elected <a href="https://en.wikipedia.org/wiki/Dmitry_Medvedev#:~:text=Corruption%20allegations">marionette alcoholist president with interest in Tuscanian wine</a> decides to put his imprint on the country. Since he is not capable of doing anything that is measurably positive, he decides to crackdown on smoking and to change time zones. By a new edict, the Russian time zones will, henceforth, permanently observe daylight saving time (DST), also in the winter. Also, <a href="https://www.youtube.com/watch?v=EV4N2dk0cMk">the official language of San Marcos will be Swedish,</a> but that is irrelevant to our topic right now.</p>

<p>What is relevant is that while your planned event is still supposed to occur at 18:00 on the 25th of November, it will now occur at a <em>different UTC time.</em> Namely - it will take place one hour earlier, because of the DST alteration for the timezone. What this means is:</p>

<ul>
  <li>If there was an alarm your device should have given you, it should now happen at a different time</li>
  <li>If there is a calculation of time you spend on something, it needs to be adjusted</li>
  <li>If there are other events which should be properly lined up with your appearance at 18:00 - like travel <em>from a different timezone</em> - you may need to review it. If your flight arrives from Europe at 17:00 local - congratulations, it now likely arrives at 18:00 instead, and you won’t make it to the event on time.</li>
</ul>

<p>This is why the conversion from a time in time zone to UTC is bound to <em>the point in time when you do the conversion.</em></p>

<p>Now let’s fast-forward a decade or more. You know that some kind of event took place in Moscow at 18:00 local time, on the 25th of November 2011. At that event, a <a href="https://en.wikipedia.org/wiki/Pitch_drop_experiment">pitch drop experiment</a> involving a very interesting type of Black Goop™ has been started, and you want to know how much time has passed since then.</p>

<p>You look at the state of time zones <em>right now</em> and the current time zone definition for Moscow tells you that Russia observes daylight-saving time, and at the time of event it should not have been in effect. You do your computation… and you are now off by an hour! Why?</p>

<p>Because, after a couple of years of learning Swedish, the previous ageing dictator has assumed his post yet again and has determined that this timezones nonsense construed by his marionette appointee was just a bunch of nonsense, and swiftly <em>reverted</em> the decision. DST was to be made summer-only again, and <a href="https://en.wikipedia.org/wiki/Nineteen_Eighty-Four">we have always been at war with Eastasia.</a> Which means that neither your calculation from 2010 <em>nor</em> your calculation from today would be correct if you only rely on the timezone information from the time you do it.</p>

<p>What you need to do instead is follow the data-over-time approach and follow the intervals, like so:</p>

<ul>
  <li>Calculate what timezone was - or is going to be - in effect at a particular <em>location</em> at a particular <em>local time</em></li>
  <li>Use that timezone to convert from local time to UTC</li>
</ul>

<p>If you need to set an alarm, a database task, or do an operation that involves durations, you need to follow the same procedure.</p>

<p><strong>This</strong> is why timezones are hard. But they are possible to grok if we approach the challenge from this temporal perspective.</p>

<p>It also means that you <em>must</em> keep your tzinfo data up to date, and the unpleasant bit is that you have to do it across your entire stack: from the OS to the database server to your Ruby gem versions. Because it carries a dataset which changes over time!</p>

<h2 id="at-the-start-of-every-workday">At the start of every workday</h2>

<p>Let’s do an exercise. Suppose we want to send our user some kind of update at the start of their workday.</p>

<p>Very important to understand: with recurring events in a timezone (or timezones) you can have <em>either</em> regular intervals (~24 hours between occurrences) <em>or</em> events occurring at the same local time <em>but not both.</em> Think from the product perspective here - <em>which one do you want for this user experience,</em> and pick accordingly.</p>

<p>If we are good with the “at this time” approach, it involves the following steps:</p>

<ul>
  <li>What is their preferred timezone?</li>
  <li>What is considered “workday” for them?</li>
</ul>

<p>The first item is important and a good thing to tackle. Modern browsers do provide you with the timezone information like so:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">tz</span> <span class="o">=</span> <span class="nx">Intl</span><span class="p">.</span><span class="nc">DateTimeFormat</span><span class="p">().</span><span class="nf">resolvedOptions</span><span class="p">().</span><span class="nx">timeZone</span><span class="p">;</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">tz</span><span class="p">);</span>
</code></pre></div></div>

<p>This can be shuttled into a hidden form input, or into a cookie, or into a <code class="language-plaintext highlighter-rouge">fetch()</code> request parameter easily enough. Note that the name you will get will not be the name of the time zone provided by the Rails <code class="language-plaintext highlighter-rouge">time_zone_select</code> - but the IANA time zone identifier. This is important. Compare:</p>

<ul>
  <li>In Rails parlance, New York is <code class="language-plaintext highlighter-rouge">Eastern Time (US &amp; Canada)</code></li>
  <li>In IANA parlance, New York is <code class="language-plaintext highlighter-rouge">America/New_York</code></li>
</ul>

<p>Out of these two, always (always!) pick the latter, because this is what the TZInfo database contains and what most software uses to resolve time zones. Save it into a column on your <code class="language-plaintext highlighter-rouge">User</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">User</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="k">def</span> <span class="nf">time_zone</span><span class="o">=</span><span class="p">(</span><span class="n">zone_or_name</span><span class="p">)</span>
    <span class="k">case</span> <span class="n">zone_or_name</span>
    <span class="k">when</span> <span class="no">String</span>
      <span class="n">as_tz</span> <span class="o">=</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TimeZone</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">zone_or_name</span><span class="p">)</span>
      <span class="n">write_attribute</span><span class="p">(</span><span class="s2">"time_zone"</span><span class="p">,</span> <span class="n">as_tz</span><span class="p">.</span><span class="nf">tzinfo</span><span class="p">.</span><span class="nf">name</span><span class="p">)</span>
    <span class="k">when</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TimeZone</span>
      <span class="n">write_attribute</span><span class="p">(</span><span class="s2">"time_zone"</span><span class="p">,</span> <span class="n">zone_or_name</span><span class="p">.</span><span class="nf">tzinfo</span><span class="p">.</span><span class="nf">name</span><span class="p">)</span>
    <span class="k">when</span> <span class="no">TZInfo</span><span class="o">::</span><span class="no">DataTimezone</span>
      <span class="n">write_attribute</span><span class="p">(</span><span class="s2">"time_zone"</span><span class="p">,</span> <span class="n">zone_or_name</span><span class="p">.</span><span class="nf">name</span><span class="p">)</span>
    <span class="k">else</span>
      <span class="n">write_attribute</span><span class="p">(</span><span class="s2">"time_zone"</span><span class="p">,</span> <span class="s2">"UTC"</span><span class="p">)</span> <span class="c1"># or raise</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">time_zone</span>
    <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TimeZone</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">read_attribute</span><span class="p">(</span><span class="s2">"time_zone"</span><span class="p">)</span> <span class="o">||</span> <span class="s2">"UTC"</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Notice how we always store the IANA identifier - this can be done also from the browser name.</p>

<p>Next, we need to figure out the workdays. For most countries, we can assume Monday to Friday (inclusive) for the moment, but it is a good idea to place this somewhere:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">workdays</span> <span class="o">=</span> <span class="sx">%w( Monday Tuesday Wednesday Thursday Friday)</span>
</code></pre></div></div>

<p>This can be modified later, but will do for the moment. We will also codify what we consider “start of working day” - let’s assume it is 9:00 in the morning:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">times</span> <span class="o">=</span> <span class="sx">%w( 9:00 )</span>
</code></pre></div></div>

<p>Next comes the actual scheduling. Which is easy, but we have to use a tool which will:</p>

<ul>
  <li>Find the moment from which we are computing the time of the next occurrence</li>
  <li>Find the timezone configuration at time of the event</li>
  <li>Do a conversion into UTC using that information and give us a UTC timestamp</li>
</ul>

<p>The perfect tool for this in the Ruby world is <a href="https://github.com/floraison/fugit">fugit</a> and if you are using any kind of ActiveJob adapter that allows scheduling or cron tables - it is likely already present in your <code class="language-plaintext highlighter-rouge">Gemfile.lock</code>, you just need to use it properly.</p>

<p>To get a timestamp, we need to compose a definition of our recurrence pattern that Fugit can parse. With our powers we can do it like this for any of our users:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Build a pattern which looks like</span>
<span class="c1"># "9:00 every Tuesday,Wednesday in America/New_York"</span>
<span class="n">pattern</span> <span class="o">=</span> <span class="s2">"</span><span class="si">#{</span><span class="n">times</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s1">','</span><span class="p">)</span><span class="si">}</span><span class="s2"> every </span><span class="si">#{</span><span class="n">workdays</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s1">','</span><span class="p">)</span><span class="si">}</span><span class="s2"> in </span><span class="si">#{</span><span class="n">user</span><span class="p">.</span><span class="nf">time_zone</span><span class="p">.</span><span class="nf">tzinfo</span><span class="p">.</span><span class="nf">name</span><span class="si">}</span><span class="s2">"</span>
<span class="n">cron</span> <span class="o">=</span> <span class="no">Fugit</span><span class="p">.</span><span class="nf">do_parse_cronish</span><span class="p">(</span><span class="n">pattern</span><span class="p">)</span>
<span class="n">reference_time</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">current</span>
<span class="n">occurs_at_utc</span> <span class="o">=</span> <span class="n">cron</span><span class="p">.</span><span class="nf">next_time</span><span class="p">(</span><span class="n">reference_time</span><span class="p">).</span><span class="nf">utc</span>
</code></pre></div></div>

<p>Pretty easy, and there is a multitude of edge cases Fugit will take care of. For example: there are hours and even <em>days</em> which disappear on timezone transitions or DST transitions. The event will be skipped if that happens, which is likely what you want.</p>

<h2 id="to-summarize">To summarize</h2>

<p>You can absolutely do recurring events for users in their time zone. Just don’t compute the intervals yourself, and remember that timezones change over time.</p>

<p>Time to go now.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Doing something at a time convenient for the user is a recurring (sic!) challenge with web applications. And the more users you have across a multitude of time zones, the more pressing it becomes to do it well. It is actually not that hard, but it does have a few fiddly bits which can be challenging to put together. So, let’s do some time traveling.]]></summary></entry><entry><title type="html">Drive manually</title><link href="https://blog.julik.nl/2025/09/driving-by-hand" rel="alternate" type="text/html" title="Drive manually" /><published>2025-09-05T00:00:00+00:00</published><updated>2025-09-05T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/09/driving-by-hand</id><content type="html" xml:base="https://blog.julik.nl/2025/09/driving-by-hand"><![CDATA[<p><a href="https://fr.wikipedia.org/wiki/Accident_de_m%C3%A9tro_du_30_ao%C3%BBt_2000_%C3%A0_Paris">On the 30th of August, 2000,</a> a train flew out of the tunnel at a station in Paris, and, without stopping, rolled on through into the tunnel. It was clear that the train was speeding, and speeding severely. Inside the tunnel, the train has derailed with the front cab car turning over. Luckily, there were no fatalities. 24 people were injured.</p>

<p>The investigation was able to establish that the train’s speed was above the maximum permitted on that section of track by 20 km/h, if not more. How could this happen, given that the Paris Metro has been equipped with automatic speed control since the early 70s?</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>It turned out that while the automatic control system was in place, it hasn’t been operational for 8 months. And it turned out that the driver of the accident train was not able to drive the train manually well enough, has lost concentration and - consequently - caused the train to accelerate way above the permitted speed.</p>

<p>Since the accident, the RATP has instated a rule for the Paris Metro drivers that, even with a present and functioning ATC (automatic train control), at least one trip up- and down the line per day should be performed under manual operation, with the driver controlling braking and traction.</p>

<p>The concept is not unfamiliar for airlines, either. For example, one airline requires its pilots to fly manual (without the use of autopilot) starting at a relatively high altitude - watch the clip from Pascal <a href="https://www.youtube.com/watch?v=MkkfC6B-pn0&amp;t=1392s">here</a> - which is especially relevant on their long-haul fleet, where flights are long and consist mostly of supervising automation, so not losing the motor and cognitive skills is vital!</p>

<p>Another well-known example comes from the world of nuclear power plants. In many facilities, operators are required to periodically run through manual control drills, where they must operate critical systems without the aid of automated safety and regulation mechanisms. The drills are often mandated by regulatory authorities and are a core part of ongoing operator certification.</p>

<h2 id="how-is-this-relevant-for-us">How is this relevant for us?</h2>

<p>In our brave new world of LLM prowess, multiple people notice that with abundant LLM output that the “magic box” is all too eager to spew out, the skills we don’t exercise frequently - like, ahem, <em>writing the damn code</em> - tend to wither. Do enough LLM-assisted coding and they may even start to atrophy completely. There is no question whether we are becoming dependent on the LLMs - there is a question of the cost of that dependence and the extent, given how the immense amount of compute it consumes <a href="https://www.forbes.com/sites/craigsmith/2023/09/08/what-large-models-cost-you--there-is-no-free-ai-lunch/">is essentially subsidized.</a></p>

<p>It is no surprise, then, that if the LLM is not available (bad connection, running out of costs, model provider is experiencing issues) finding ourselves writing code can pose a challenge! And it’s not a “millennial gets annoyed at discomfort” thing at all – it is the case of humans failing to operate in an environment with tool substitutions! It is a fairly classic problem.</p>

<p>So since I’ve started doing a lot of LLM-assisted coding, my mantra has become that I will do at least 30 minutes of <em>actual manual coding</em> on any given day that I work on software. I specifically forbid the LLM from making code changes for me, but ask it to “reason” with me instead. To do so, I use this finishing sentence in the prompts:</p>

<blockquote>
  <p>Do not write any code, explain the approach to me first.</p>
</blockquote>

<p>Afterwards I will go and either very carefully (and selectively) copy and paste the LLM’s proposition into the editor, or even type it out by hand.</p>

<p>Make it a habit to drive manually, regularly – and if needed force yourself to do so. It may save your bacon down the line. Even if it is not line 12 of the RATP.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[On the 30th of August, 2000, a train flew out of the tunnel at a station in Paris, and, without stopping, rolled on through into the tunnel. It was clear that the train was speeding, and speeding severely. Inside the tunnel, the train has derailed with the front cab car turning over. Luckily, there were no fatalities. 24 people were injured. The investigation was able to establish that the train’s speed was above the maximum permitted on that section of track by 20 km/h, if not more. How could this happen, given that the Paris Metro has been equipped with automatic speed control since the early 70s?]]></summary></entry><entry><title type="html">Illegible perception</title><link href="https://blog.julik.nl/2025/09/illegible-perception" rel="alternate" type="text/html" title="Illegible perception" /><published>2025-09-05T00:00:00+00:00</published><updated>2025-09-05T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/09/illegible-perception</id><content type="html" xml:base="https://blog.julik.nl/2025/09/illegible-perception"><![CDATA[<p>Good ideas are worth disagreeing with. Sean Goedecke posits that <a href="https://www.seangoedecke.com/seeing-like-a-software-company/">organizations implement process to have legibility</a> – a very interesting idea. However, I see it differently.</p>

<p>It is not about <em>legibility</em> but it is about <em>perception of legibility.</em> Does the CEO look at burndown charts in your project management tool? Maybe, but highly unlikely. Does the head of product look at the key user flows that are getting implemented? Maybe. Does the EM look at the backlog of technical debt and prioritize items out of that backlog? Maybe, but if the organization is small enough and the EM is hands-on and smart - they “just go and do things”.</p>

<p>In nearly all the situations I’ve seen, the implementation of processes targeted at <em>legibility</em> were actually optics in disguise. We are now writing user stories because the person who became the CPO wants to establish that practice. We have a this-or-that process because the executive – or the key stakeholder – wants it implemented in a specific way. Or - because they do not care</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>I am not discounting the shared perceptions either! For example, it might be that multiple stakeholders want to have the perception that an incident reporting process is in place. They will not track whether people can actually learn anything from the incident reports, but they will hold the impression that it is in place.</p>

<p>Many introduced processes I’ve seen in software organizations were the result of someone’s <em>perception</em> being carefully managed, with the objective outcomes (if there were any) ranging from suboptimal to problematic.</p>

<p>Users can’t login into the application for 3 hours. Yet, because the CTO has the <em>perception</em> that the incident is carefully managed (he sees people pasting things in Slack about the issue) they do not register that what is actually needed is working access to the production system, which is only available to a select 2 people in the organization. One is on holiday, the other is swamped with other work. But because they overfocus on the <em>perception</em> that things are going well – they truck on. 3 hours turn into 4, then 5. Once things are finally fixed, another process rule gets instated - that there be an alert on failed logins.</p>

<p>A mistake one can make is that in this case <em>legibility</em> is not being achieved. In actuality, what is happening is <em>exactly</em> what the organization and the stakeholders wanted - the correct <em>perception</em> has been created that makes stakeholders <em>feel good.</em></p>

<h2 id="operating-on-perception-is-natural">Operating on perception is natural</h2>

<p>Humans are very visceral creatures. Two-system thinking is <a href="https://en.wikipedia.org/wiki/Thinking%2C_Fast_and_Slow">absolutely a thing</a> and there is one very particular property: deep thinking – the “slow thinking” approach to finding solutions or answers – is expensive. It takes a lot of time, and since humans don’t really operate in parallel - but operate concurrently - it pushes a lot of other important tasks back onto the queue. Therefore, most people not only operate on feelings, hunches and perceptions - it is actually demanded of them. When you have an incident or a peculiar humans problem in your organization, you are not expected to say “I will go into seclusion for 3 days and come back with a plan” - you are expected to provide your instinctual response as soon as possible, ideally within the few minutes.</p>

<p>Grasping onto a <em>perception</em> - your understanding of the environment which <em>makes you feel good</em> – is absolutely natural in this case. Moreover: since it gives you a sense of completion and a dopamine hit (“I know how to sort this!”) – <em>other</em> people know too that it makes you <em>feel good</em> and will play to that. A cardinal rule of corporations is to <em>not upset your manager and to not make them look bad</em> – but that is basic competence. Skilled professionals know to take this a step further, and do so effectively – they <em>make their manager feel confident.</em></p>

<p>Proposing a process which makes you, as a decision maker, <em>perceive</em> your organization is becoming more legible is a very juicy thing to do for a midlevel manager who reports to you. They know that it is very hard to actually evaluate the outcome of <em>increasing legibility</em> - you could make metrics on the <em>output</em> (“now 85% of work is tracked in JIRA”) but you can’t make them on the <em>outcome</em> (“now every engineer knows where to find the feature briefs”, or - for second-order effects - “now developers spend 20% of their time managing the JIRA process, and that time was previously dedicated to building”). Note the present tense here – it is deliberate. But the goal - <em>making you feel good</em> - has been achieved. You now firmly believe that the person has introduced something <em>good</em> – mission accomplished. This naturally has positive effects on the person’s promotion opportunities in the future.</p>

<p>The issue is that a huge JIRA installation with a multitude of projects does not make the organization more legible – or, maybe it does – but you never check. The goal was not making the organization more legible, the goal was <em>creating a perception with you.</em></p>

<p>If you are in a position of authority it is wise to track all the “proposals” of such “legibility increases” with some additional vigilance. Some processes are good, and they <em>do</em> increase legibility and visibility. But many processes will be proposed to create <em>optics</em> of something, or – to advance careers by <em>making you feel confident.</em> And this may be fine as well – just don’t expect there to be real legibility improvements.</p>

<h2 id="it-is-a-balance">It is a balance</h2>

<p>I’m not trying to say that it’s all optics, smoke, and mirrors. Process improvements very frequently bring good with them. A well-prepared improvement with a good definition of progress (and legibility) can free up hours of work, which can then be used for something more profitable for the organization. Other improvements can fix the completely broken processes and deliver you a story similar to <a href="https://www.goodreads.com/book/show/44333183-the-unicorn-project">The Unicorn Project</a> and some improvements are so apparently useful that there can’t even be a discussion about them being “fake” or “pretending”.</p>

<p>Just a few examples:</p>

<ul>
  <li>It is likely that having feature specifications will make stakeholders demanding those features more responsible</li>
  <li>It is likely that shortening the time-to-live for deployments is going to allow the org to deliver faster</li>
  <li>It is likely that having incident reports will make it more transparent - both to stakeholders and to line employees - where the system’s weak spots are</li>
</ul>

<p>The challenge is gauging how much of a particular initiative’s output is going to be <em>perception</em> and how much is going to be <em>legibility</em> and being honest to yourself about it. Both on the initiating end and on the executing end of the initiative. And what’s in it <em>for you</em> and what goals are relevant <em>for you</em> once the process gets installed / improvement gets made.</p>

<p>Being honest with yourself is usually a good thing, and if you don’t project outward too much - it won’t even impact your career advancement.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Good ideas are worth disagreeing with. Sean Goedecke posits that organizations implement process to have legibility – a very interesting idea. However, I see it differently. It is not about legibility but it is about perception of legibility. Does the CEO look at burndown charts in your project management tool? Maybe, but highly unlikely. Does the head of product look at the key user flows that are getting implemented? Maybe. Does the EM look at the backlog of technical debt and prioritize items out of that backlog? Maybe, but if the organization is small enough and the EM is hands-on and smart - they “just go and do things”. In nearly all the situations I’ve seen, the implementation of processes targeted at legibility were actually optics in disguise. We are now writing user stories because the person who became the CPO wants to establish that practice. We have a this-or-that process because the executive – or the key stakeholder – wants it implemented in a specific way. Or - because they do not care]]></summary></entry><entry><title type="html">Turning your Apple Calendar into a time tracker</title><link href="https://blog.julik.nl/2025/08/turning-apple-calendar-into-time-tracker" rel="alternate" type="text/html" title="Turning your Apple Calendar into a time tracker" /><published>2025-08-22T00:00:00+00:00</published><updated>2025-08-22T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/08/turning-apple-calendar-into-time-tracker</id><content type="html" xml:base="https://blog.julik.nl/2025/08/turning-apple-calendar-into-time-tracker"><![CDATA[<p>In the brave new world of self-employment one thing I found very important is getting a grip on time. In general, this turns out to be the biggest challenge for me personally - not having kids and no longer living with a partner I have way more free time than is customary for a 40+ year old, and it shows. And it is becoming more important to get a good understanding of both where the time gets spent, and how much of that time is billable.</p>

<p>In the past I used to use <a href="https://nokotime.com/">Noko</a> for time tracking, but I found myself cooling down on it. I didn’t really enjoy having a subscription, the jobs I had to do were very sporadic and I no longer enjoyed using it. I’ve looked at a few time tracking packages but was generally put off by subscriptions, the perpetual “onlineness” of them all, and the fact that they were desperately selling to businesses, not consumers like myself. And – got to be frank about it – I am a sucker for not only local-first apps, but native apps.</p>

<p>And then an idea struck me: I am actually using the standard Apple Calendar, with online sync via iCloud, for my calendar needs. If I could add the entries to the calendar, and then tally them - this would give me a great time tracking solution, with the remaining work being generating the invoice from the tally of hours. And, despite Apple turning macOS into a prison of Duplo bricks year over year, there is still <em>some</em> automation available to make this work. So, shall we?</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="the-forgotten-magic-of-osa">The forgotten magic of OSA</h2>

<p>Back when computers were personal, Facebook was non-existent and you could buy an application license and own it in perpetuity, macOS had a great feature embedded in it that quite a few people enjoyed: AppleScript. It was their answer to the VisualBasic scripting provided by Windows, and in some ways it was better than VB. The basics worked like this:</p>

<ul>
  <li>Every application (and some system services) would expose an object model you could script against</li>
  <li>You would then use a peculiar language called AppleScript to address that object model</li>
  <li>The output of your script would not be something on standard output, but manipulations you would do in another application</li>
</ul>

<p>In the brief period of macOS renaissance (what I would call the Avi Tevanian era) the model was expanded and the requirement to write things in AppleScript was dropped - the system became what is now known as the <a href="https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/HowMacScriptingWorks.html">Open Scripting Architecture</a> – or OSA, for short. You could talk to applications using any language of your choice, because the scripting functionality has gotten access to basic primitives exported by the operating system frameworks - so even with little effort by the developer, a world of interesting automations opened up.</p>

<p>And it was sorely needed. AppleScript was a language from a cohort of early 90-s programming languages which presumed that using “basic English” instead of language keywords would make the language more accessible. What it did instead was make it incredibly verbose. A hallmark of these languages was, surprisingly, using <code class="language-plaintext highlighter-rouge">the</code> to indicate that a variable contains a single value and is not a list. Behold:</p>

<div class="language-applescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">set the clipboard to</span><span class="w"> </span><span class="s2">"Add this sentence at the end."</span><span class="w">
</span><span class="k">tell</span><span class="w"> </span><span class="nb">application</span><span class="w"> </span><span class="s2">"TextEdit"</span><span class="w">
    </span><span class="nb">activate</span><span class="w">  </span><span class="c1">--make sure TextEdit is running</span><span class="w">
    </span><span class="nb">make</span><span class="w"> </span><span class="nb">new</span><span class="w"> </span><span class="nb">paragraph</span><span class="w"> </span><span class="nb">at</span><span class="w"> </span><span class="k">end</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="na">document</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="nv">with</span><span class="w"> </span><span class="nb">data</span><span class="w"> </span><span class="p">(</span><span class="nb">return</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nb">the clipboard</span><span class="p">)</span><span class="w">
</span><span class="k">end</span><span class="w"> </span><span class="k">tell</span><span class="w">
</span></code></pre></div></div>

<p>In this case <code class="language-plaintext highlighter-rouge">the</code> indicates that <code class="language-plaintext highlighter-rouge">clipboard</code> is a “universe-wide” singleton. I don’t generally adore Dijkstra’s statements on the practical side of programming, but I tend to agree with his views <a href="https://www.cs.utexas.edu/~EWD/transcriptions/EWD06xx/EWD667.html">on natural language programming</a></p>

<p>Anyway, with the apparition of macOS X and its newer versions, AppleScript could finally be left behind and you could actually talk to the OSA backbone using any other language. For example, Apple would ship bindings to Cocoa and you could, at some point, do this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'osx/cocoa'</span><span class="p">;</span> <span class="kp">include</span> <span class="no">OSX</span>
<span class="n">require_framework</span> <span class="s1">'ScriptingBridge'</span>

<span class="n">finder</span> <span class="o">=</span> <span class="no">SBApplication</span><span class="p">.</span><span class="nf">applicationWithBundleIdentifier</span><span class="p">(</span><span class="s1">'com.apple.finder'</span><span class="p">)</span>
<span class="n">destination</span> <span class="o">=</span> <span class="n">finder</span><span class="p">.</span><span class="nf">home</span><span class="p">.</span><span class="nf">folders</span><span class="p">.</span><span class="nf">objectWithName</span><span class="p">(</span><span class="s1">'Documents'</span><span class="p">)</span>
<span class="n">finder</span><span class="p">.</span><span class="nf">desktop</span><span class="p">.</span><span class="nf">files</span><span class="p">.</span><span class="nf">get</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span>
   <span class="n">f</span><span class="p">.</span><span class="nf">moveTo_replacing_positionedAt_routingSuppressed</span><span class="p">(</span><span class="n">destination</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="kp">nil</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>All of that has died with the Duplo-ification and consumerization of the Mac, but one escape hatch has remained: you still can use JavaScript! And a relatively modern one at that, which is neat.</p>

<h2 id="getting-at-your-calendar">Getting at your calendar</h2>

<p>The Calendar is just an application that is accessible by its name. We start with grabbing a specific calendar and having a prefix for the events we want to search for.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">Calendar</span> <span class="o">=</span> <span class="nc">Application</span><span class="p">(</span><span class="dl">"</span><span class="s2">Calendar</span><span class="dl">"</span><span class="p">)</span>

<span class="kd">let</span> <span class="nx">calendarName</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">me</span><span class="dl">"</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">eventNamePrefix</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">AcmeIndustries</span><span class="dl">"</span><span class="p">;</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">calendarName</code> is the name of the calendar visible in your Calendar.app in the pane:</p>

<p><img src="/assets/2025-08-22-timetrack/calendars.png" alt="List of calendars in the Apple Calendar sidebar" /></p>

<p>Next comes the interesting part. We need to query for events in the calendar that have started after a certain point in time and that satisfy the name prefix. To do so, we need to use something called <em>predicates</em> - this is <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Predicates/Articles/pCreating.html">a weird Cocoa way</a> of querying datasets. It has a few downsides - it has obtuse syntax, it is slow (I suspect that every extra predicate is a linear scan, making every added predicate a power increase in algorithmic complexity – which is going to be incredibly noticeable if you keep your calendar alive for years already). But it is workable, so let’s do some <code class="language-plaintext highlighter-rouge">objc_msgsend</code> by proxy of a proxy of a proxy:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">theCalendar</span> <span class="o">=</span> <span class="nx">Calendar</span><span class="p">.</span><span class="nx">calendars</span><span class="p">.</span><span class="nf">whose</span><span class="p">({</span><span class="na">name</span><span class="p">:</span> <span class="nx">calendarName</span><span class="p">})[</span><span class="mi">0</span><span class="p">]</span>
<span class="kd">let</span> <span class="nx">fromDate</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Date</span><span class="p">(</span><span class="mi">2025</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">25</span><span class="p">)</span>
<span class="kd">let</span> <span class="nx">eventPredicate</span> <span class="o">=</span> <span class="nx">theCalendar</span><span class="p">.</span><span class="nx">events</span><span class="p">.</span><span class="nf">whose</span><span class="p">(</span>
  <span class="p">{</span><span class="na">_and</span><span class="p">:</span> <span class="p">[</span>
      <span class="p">{</span><span class="na">summary</span><span class="p">:</span> <span class="p">{</span> <span class="na">_beginsWith</span><span class="p">:</span> <span class="nx">eventNamePrefix</span> <span class="p">}},</span>
      <span class="p">{</span><span class="na">startDate</span><span class="p">:</span> <span class="p">{</span> <span class="na">_greaterThan</span><span class="p">:</span> <span class="nx">fromDate</span><span class="p">}}</span>
    <span class="p">]</span>
  <span class="p">}</span>
<span class="p">);</span>
</code></pre></div></div>

<p>I am using <code class="language-plaintext highlighter-rouge">theCalendar</code> as a cheeky name owing to the AppleScript heritage. Note that we immediately do the postfix indexing for <code class="language-plaintext highlighter-rouge">[0]</code> - this is important. A predicate is just that - a predicate. Think about it as you would think about a prepared SQL query - the system has compiled it, but didn’t execute it yet. To execute the predicate you either call it (because, believe it or not, the thing you get back from <code class="language-plaintext highlighter-rouge">whose</code> is both a function and an indexable object!) or index into it.</p>

<p>Then, for our events, we need a compoind predicate. In this case we want all events whose <code class="language-plaintext highlighter-rouge">summary</code> - which is the title of the event - begins with our prefix. Why an underscore is needed at the start - I don’t know, but in general the OSA thing is somewhat janky, and I tend to just accept it for what it is. For the start date we apply a <code class="language-plaintext highlighter-rouge">greaterThan</code> condition which is going to add a cutoff to our search, that speeds up our predicate considerably.</p>

<p>Next, we resolve our predicate - which returns us handles to <code class="language-plaintext highlighter-rouge">Event</code> objects. Those objects are <em>also</em> functions and should be called to be resolved! But it does work. And, as a bonus, JS syntax supported by OSA is fairly modern - it has a builtin JSON module, <code class="language-plaintext highlighter-rouge">let</code>, <code class="language-plaintext highlighter-rouge">const</code> and object shorthands.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">pertinentEvents</span> <span class="o">=</span> <span class="nf">eventPredicate</span><span class="p">();</span>
<span class="k">for </span><span class="p">(</span><span class="kd">var</span> <span class="nx">evtResolver</span> <span class="k">of</span> <span class="nx">pertinentEvents</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="nx">evtHandle</span> <span class="o">=</span> <span class="nf">evtResolver</span><span class="p">();</span>
  <span class="kd">let</span> <span class="nx">evt</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">uid</span><span class="p">:</span> <span class="nx">evtHandle</span><span class="p">.</span><span class="nf">uid</span><span class="p">(),</span>
    <span class="na">summary</span><span class="p">:</span> <span class="nx">evtHandle</span><span class="p">.</span><span class="nf">summary</span><span class="p">(),</span>
    <span class="na">startDate</span><span class="p">:</span> <span class="nx">evtHandle</span><span class="p">.</span><span class="nf">startDate</span><span class="p">().</span><span class="nf">toISOString</span><span class="p">(),</span>
    <span class="na">endDate</span><span class="p">:</span> <span class="nx">evtHandle</span><span class="p">.</span><span class="nf">endDate</span><span class="p">().</span><span class="nf">toISOString</span><span class="p">(),</span>
  <span class="p">};</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">(</span><span class="nx">evt</span><span class="p">));</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">(</span><span class="nx">materializedEvents</span><span class="p">));</span>
</code></pre></div></div>

<p>You may notice that I deliberately keep this script short and make it use <code class="language-plaintext highlighter-rouge">console.log()</code> to output the events in a digestible JSON format (technically, our output is going to be <a href="https://jsonlines.org/">JSONlines</a>) - that is for a good reason.</p>

<p>OSA is <em>slow.</em> Very, <em>very</em> slow. The error messages it outputs are obtuse and often somewhat cryptic. There is no proper debugging (there is some, but it is also pretty bad), and the fact that you need to call a property of an object instead of accessing it directly adds to the confusion. Therefore, I vastly prefer retrieving all the data I need, outputting it in some common format, and then doing the processing in an environment with a better DX.</p>

<p>We save our script into <code class="language-plaintext highlighter-rouge">extract-events.osa.js</code> and then we can run it using <code class="language-plaintext highlighter-rouge">osascript</code> from the Terminal, like so:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>osascript <span class="nt">-l</span> JavaScript extract-events.osa.js
</code></pre></div></div>

<p>It is also a good idea to allow our script to accept arguments. To do that, we need to wrap our entire script into a <code class="language-plaintext highlighter-rouge">run</code> function - it will get the ARGV array as its argument:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">run</span><span class="p">(</span><span class="nx">argvArray</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="p">[</span><span class="nx">calendarName</span><span class="p">,</span> <span class="nx">eventNamePrefix</span><span class="p">,</span> <span class="nx">fromDateStr</span><span class="p">]</span> <span class="o">=</span> <span class="nx">argvArray</span><span class="p">;</span>
  <span class="kd">let</span> <span class="nx">fromDate</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Date</span><span class="p">(</span><span class="nx">fromDateStr</span><span class="p">)</span>

  <span class="kd">let</span> <span class="nx">Calendar</span> <span class="o">=</span> <span class="nc">Application</span><span class="p">(</span><span class="dl">"</span><span class="s2">Calendar</span><span class="dl">"</span><span class="p">)</span>
  <span class="kd">let</span> <span class="nx">theCalendar</span> <span class="o">=</span> <span class="nx">Calendar</span><span class="p">.</span><span class="nx">calendars</span><span class="p">.</span><span class="nf">whose</span><span class="p">({</span><span class="na">name</span><span class="p">:</span> <span class="nx">calendarName</span><span class="p">})[</span><span class="mi">0</span><span class="p">]</span>
  <span class="kd">let</span> <span class="nx">eventPredicate</span> <span class="o">=</span> <span class="nx">theCalendar</span><span class="p">.</span><span class="nx">events</span><span class="p">.</span><span class="nf">whose</span><span class="p">(</span>
    <span class="p">{</span><span class="na">_and</span><span class="p">:</span> <span class="p">[</span>
        <span class="p">{</span><span class="na">summary</span><span class="p">:</span> <span class="p">{</span> <span class="na">_beginsWith</span><span class="p">:</span> <span class="nx">eventNamePrefix</span> <span class="p">}},</span>
        <span class="p">{</span><span class="na">startDate</span><span class="p">:</span> <span class="p">{</span> <span class="na">_greaterThan</span><span class="p">:</span> <span class="nx">fromDate</span><span class="p">}}</span>
      <span class="p">]</span>
    <span class="p">}</span>
  <span class="p">);</span>
  <span class="c1">// Resolve the array</span>
  <span class="kd">let</span> <span class="nx">totalHours</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
  <span class="kd">let</span> <span class="nx">pertinentEvents</span> <span class="o">=</span> <span class="nf">eventPredicate</span><span class="p">();</span>
  <span class="kd">let</span> <span class="nx">materializedEvents</span> <span class="o">=</span> <span class="p">[];</span>
  <span class="k">for </span><span class="p">(</span><span class="kd">var</span> <span class="nx">evtResolver</span> <span class="k">of</span> <span class="nx">pertinentEvents</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">let</span> <span class="nx">evtHandle</span> <span class="o">=</span> <span class="nf">evtResolver</span><span class="p">();</span>
    <span class="kd">let</span> <span class="nx">evt</span> <span class="o">=</span> <span class="p">{</span>
      <span class="na">uid</span><span class="p">:</span> <span class="nx">evtHandle</span><span class="p">.</span><span class="nf">uid</span><span class="p">(),</span>
      <span class="na">summary</span><span class="p">:</span> <span class="nx">evtHandle</span><span class="p">.</span><span class="nf">summary</span><span class="p">(),</span>
      <span class="na">startDate</span><span class="p">:</span> <span class="nx">evtHandle</span><span class="p">.</span><span class="nf">startDate</span><span class="p">().</span><span class="nf">toISOString</span><span class="p">(),</span>
      <span class="na">endDate</span><span class="p">:</span> <span class="nx">evtHandle</span><span class="p">.</span><span class="nf">startDate</span><span class="p">().</span><span class="nf">toISOString</span><span class="p">(),</span>
    <span class="p">};</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">(</span><span class="nx">evt</span><span class="p">));</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>and we pass our arguments in order:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>osascript <span class="nt">-l</span> JavaScript extract-events.js superjulik <span class="s2">"AcmeIndustries"</span> 2025-08-01
</code></pre></div></div>

<p>and we get some terminal output:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>osascript <span class="nt">-l</span> JavaScript extract-events.osa.js superjulik <span class="s2">"AcmeIndustries - "</span> 2025-08-01
<span class="o">{</span><span class="s2">"uid"</span>:<span class="s2">"43804A58-6207-4D22-A3A1-B1433DA1CB03"</span>,<span class="s2">"summary"</span>:<span class="s2">"AcmeIndustries - Processing Flow Rework"</span>,<span class="s2">"startDate"</span>:<span class="s2">"2025-08-04T15:45:00.000Z"</span>,<span class="s2">"endDate"</span>:<span class="s2">"2025-08-04T15:45:00.000Z"</span><span class="o">}</span>
<span class="o">{</span><span class="s2">"uid"</span>:<span class="s2">"CE846421-4246-4BBB-B4A7-855CCEC54286"</span>,<span class="s2">"summary"</span>:<span class="s2">"AcmeIndustries - Generative flow bug investigation"</span>,<span class="s2">"startDate"</span>:<span class="s2">"2025-08-12T21:30:00.000Z"</span>,<span class="s2">"endDate"</span>:<span class="s2">"2025-08-12T21:30:00.000Z"</span><span class="o">}</span>
<span class="o">{</span><span class="s2">"uid"</span>:<span class="s2">"0126979A-BCF3-4627-96A6-6B3D7ECDCFB8"</span>,<span class="s2">"summary"</span>:<span class="s2">"AcmeIndustries - Processing Flow"</span>,<span class="s2">"startDate"</span>:<span class="s2">"2025-08-17T17:00:00.000Z"</span>,<span class="s2">"endDate"</span>:<span class="s2">"2025-08-17T17:00:00.000Z"</span><span class="o">}</span>
<span class="o">{</span><span class="s2">"uid"</span>:<span class="s2">"D04253FB-33DC-425A-9E5D-28E6F123616F"</span>,<span class="s2">"summary"</span>:<span class="s2">"AcmeIndustries - Processing Flow refactor"</span>,<span class="s2">"startDate"</span>:<span class="s2">"2025-08-21T11:30:00.000Z"</span>,<span class="s2">"endDate"</span>:<span class="s2">"2025-08-21T11:30:00.000Z"</span><span class="o">}</span>
<span class="o">{</span><span class="s2">"uid"</span>:<span class="s2">"6391A949-4147-4F04-AE78-28C4B635E379"</span>,<span class="s2">"summary"</span>:<span class="s2">"AcmeIndustries - Processing Flow Rework"</span>,<span class="s2">"startDate"</span>:<span class="s2">"2025-08-21T14:00:00.000Z"</span>,<span class="s2">"endDate"</span>:<span class="s2">"2025-08-21T14:00:00.000Z"</span><span class="o">}</span>
<span class="o">{</span><span class="s2">"uid"</span>:<span class="s2">"8A96EEA0-57D0-43E1-90A4-0C6FB940B2C3"</span>,<span class="s2">"summary"</span>:<span class="s2">"AcmeIndustries - Processing Flow Rework"</span>,<span class="s2">"startDate"</span>:<span class="s2">"2025-08-21T15:30:00.000Z"</span>,<span class="s2">"endDate"</span>:<span class="s2">"2025-08-21T15:30:00.000Z"</span><span class="o">}</span>
<span class="o">{</span><span class="s2">"uid"</span>:<span class="s2">"7E0B00C3-C383-4360-9906-D70B584E15B2"</span>,<span class="s2">"summary"</span>:<span class="s2">"AcmeIndustries - Processing Flow"</span>,<span class="s2">"startDate"</span>:<span class="s2">"2025-08-21T22:00:00.000Z"</span>,<span class="s2">"endDate"</span>:<span class="s2">"2025-08-21T22:00:00.000Z"</span><span class="o">}</span>
</code></pre></div></div>
<p>Tedious, painful - but it works. And goes through… <em>the official channels.</em></p>

<p>It also takes nearly 2 minutes to run. 2 minutes on an M1 MacBook Pro to recover 7 events with what amounts to a prefix search and an integer comparison. From data that is sitting there on the internal SSD (and maybe - even in memory).</p>

<h2 id="now-with-100-times-less-official-framework-scripting-garbage">Now with 100 times less Official Framework Scripting Garbage</h2>

<p>You wouldn’t expect that I’d stop there though, wouldn’t you? After all, that Apple Calendar data ought to also be stored… <em>somewhere</em> on the computer. And it ought to be stored in <em>some format</em> that allows faster and more optimized access to things than this… abomination.</p>

<p>What if that format is a SQLite database?..</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>lsof | <span class="nb">grep </span>Calendar | <span class="nb">grep </span>sql
calaccess  1086 julik  txt       REG               1,18      32768               42610 /Users/julik/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb-shm
calaccess  1086 julik    4u      REG               1,18    8454144               42582 /Users/julik/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb
calaccess  1086 julik    5u      REG               1,18     173072               42608 /Users/julik/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb-wal
calaccess  1086 julik    6u      REG               1,18      32768               42610 /Users/julik/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb-shm
dataacces  1240 julik  txt       REG               1,18      32768               42610 /Users/julik/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb-shm
dataacces  1240 julik    8u      REG               1,18    8454144               42582 /Users/julik/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb
dataacces  1240 julik    9u      REG               1,18    8454144               42582 /Users/julik/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb
dataacces  1240 julik   10u      REG               1,18      32768               42610 /Users/julik/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb-shm
dataacces  1240 julik   11u      REG               1,18    8454144               42582 /Users/julik/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb
dataacces  1240 julik   16u      REG               1,18     173072               42608 /Users/julik/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb-wal
com.apple 17267 julik  txt       REG               1,18      32768             2576443 /Users/julik/Library/Containers/com.apple.CalendarWeatherKitService/Data/Library/HTTPStorages/com.apple.CalendarWeatherKitService/httpstorages.sqlite-shm
com.apple 17267 julik    3u      REG               1,18       4096             2576439 /Users/julik/Library/Containers/com.apple.CalendarWeatherKitService/Data/Library/HTTPStorages/com.apple.CalendarWeatherKitService/httpstorages.sqlite
com.apple 17267 julik    4u      REG               1,18      32992             2576442 /Users/julik/Library/Containers/com.apple.CalendarWeatherKitService/Data/Library/HTTPStorages/com.apple.CalendarWeatherKitService/httpstorages.sqlite-wal
com.apple 17267 julik    5u      REG               1,18      32768             2576443 /Users/julik/Library/Containers/com.apple.CalendarWeatherKitService/Data/Library/HTTPStorages/com.apple.CalendarWeatherKitService/httpstorages.sqlite-shm
</code></pre></div></div>

<p>Would you look at that! So there is apparently a SQLite database with our calendar data! Let’s take a closer look:</p>

<p><img src="/assets/2025-08-22-timetrack/db-structure.png" alt="Database structure" /></p>

<p>For what we need to accomplish, we only need the <code class="language-plaintext highlighter-rouge">Calendar</code> and <code class="language-plaintext highlighter-rouge">CalendarItem</code> tables. Let’s do a query!</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">CalendarItem</span> <span class="k">WHERE</span> <span class="n">start_date</span> <span class="o">&gt;</span> <span class="p">(</span><span class="n">strftime</span><span class="p">(</span><span class="s1">'%s'</span><span class="p">,</span> <span class="s1">'2025-07-01'</span><span class="p">)</span> <span class="o">-</span> <span class="mi">978307200</span><span class="p">)</span> <span class="k">AND</span> <span class="n">end_date</span> <span class="o">&lt;</span> <span class="p">(</span><span class="n">strftime</span><span class="p">(</span><span class="s1">'%s'</span><span class="p">,</span> <span class="s1">'now'</span><span class="p">)</span> <span class="o">-</span> <span class="mi">978307200</span><span class="p">)</span>
</code></pre></div></div>

<p>What is this weird <code class="language-plaintext highlighter-rouge">978307200</code> you would ask? Well… the fields in the database are stored as epoch seconds. But - not from UNIX epoch, but from some… weird macOS epoch - namely, <code class="language-plaintext highlighter-rouge">2001-01-01 00:00:00 UTC</code> - which is, apparently, <a href="https://www.thecodeship.com/general/converting-cocoa-unix-timestamp/">a CoreData standard.</a> The fixed offset converts our input time value into a value that can be compared against in that CoreData epoch.</p>

<p>That query takes… 37 milliseconds and returns 50 records for my calendar, which is not bad compared to the scripting access to an even smaller subset of records which takes 120 seconds. We also want to query for the correct calendar:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span>
  <span class="n">CalendarItem</span><span class="p">.</span><span class="o">*</span>
<span class="k">FROM</span> <span class="n">CalendarItem</span>
<span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">Calendar</span> <span class="k">ON</span> <span class="n">Calendar</span><span class="p">.</span><span class="nv">"ROWID"</span> <span class="o">=</span> <span class="n">CalendarItem</span><span class="p">.</span><span class="n">calendar_id</span> <span class="k">AND</span> <span class="n">Calendar</span><span class="p">.</span><span class="n">title</span> <span class="o">=</span> <span class="s1">'superjulik'</span>
<span class="k">WHERE</span> <span class="n">start_date</span> <span class="o">&gt;</span> <span class="p">(</span><span class="n">strftime</span><span class="p">(</span><span class="s1">'%s'</span><span class="p">,</span> <span class="s1">'2025-07-01'</span><span class="p">)</span> <span class="o">-</span> <span class="mi">978307200</span><span class="p">)</span> <span class="k">AND</span> <span class="n">end_date</span> <span class="o">&lt;</span> <span class="p">(</span><span class="n">strftime</span><span class="p">(</span><span class="s1">'%s'</span><span class="p">,</span> <span class="s1">'now'</span><span class="p">)</span> <span class="o">-</span> <span class="mi">978307200</span><span class="p">)</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">start_date</span> <span class="k">ASC</span>
</code></pre></div></div>

<p><img src="/assets/2025-08-22-timetrack/calendar-items.png" alt="Query result" /></p>

<p>and finally, we add our clause for the event title:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span>
  <span class="n">CalendarItem</span><span class="p">.</span><span class="o">*</span>
<span class="k">FROM</span> <span class="n">CalendarItem</span>
<span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">Calendar</span> <span class="k">ON</span> <span class="n">Calendar</span><span class="p">.</span><span class="nv">"ROWID"</span> <span class="o">=</span> <span class="n">CalendarItem</span><span class="p">.</span><span class="n">calendar_id</span> <span class="k">AND</span> <span class="n">Calendar</span><span class="p">.</span><span class="n">title</span> <span class="o">=</span> <span class="s1">'superjulik'</span>
<span class="k">WHERE</span>
  <span class="n">start_date</span> <span class="o">&gt;</span> <span class="p">(</span><span class="n">strftime</span><span class="p">(</span><span class="s1">'%s'</span><span class="p">,</span> <span class="s1">'2025-07-01'</span><span class="p">)</span> <span class="o">-</span> <span class="mi">978307200</span><span class="p">)</span> <span class="k">AND</span>
  <span class="n">end_date</span> <span class="o">&lt;</span> <span class="p">(</span><span class="n">strftime</span><span class="p">(</span><span class="s1">'%s'</span><span class="p">,</span> <span class="s1">'now'</span><span class="p">)</span> <span class="o">-</span> <span class="mi">978307200</span><span class="p">)</span> <span class="k">AND</span>
  <span class="n">summary</span> <span class="k">LIKE</span> <span class="s1">'Acme%'</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">start_date</span> <span class="k">ASC</span>
</code></pre></div></div>

<p>Since we have access to all the interesting data, we can easily tally up the hours spent on <code class="language-plaintext highlighter-rouge">Acme</code>-related projects now:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">WITH</span> <span class="n">tracked_items</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span>
    <span class="n">CalendarItem</span><span class="p">.</span><span class="o">*</span>
  <span class="k">FROM</span> <span class="n">CalendarItem</span>
  <span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">Calendar</span> <span class="k">ON</span> <span class="n">Calendar</span><span class="p">.</span><span class="nv">"ROWID"</span> <span class="o">=</span> <span class="n">CalendarItem</span><span class="p">.</span><span class="n">calendar_id</span> <span class="k">AND</span> <span class="n">Calendar</span><span class="p">.</span><span class="n">title</span> <span class="o">=</span> <span class="s1">'superjulik'</span>
  <span class="k">WHERE</span>
    <span class="n">start_date</span> <span class="o">&gt;</span> <span class="p">(</span><span class="n">strftime</span><span class="p">(</span><span class="s1">'%s'</span><span class="p">,</span> <span class="s1">'2025-06-01'</span><span class="p">)</span> <span class="o">-</span> <span class="mi">978307200</span><span class="p">)</span> <span class="k">AND</span>
    <span class="n">end_date</span> <span class="o">&lt;</span> <span class="p">(</span><span class="n">strftime</span><span class="p">(</span><span class="s1">'%s'</span><span class="p">,</span> <span class="s1">'now'</span><span class="p">)</span> <span class="o">-</span> <span class="mi">978307200</span><span class="p">)</span> <span class="k">AND</span>
    <span class="n">summary</span> <span class="k">LIKE</span> <span class="s1">'Acme%'</span>
  <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">start_date</span> <span class="k">ASC</span>
<span class="p">)</span> <span class="k">SELECT</span> <span class="k">SUM</span><span class="p">(</span><span class="n">end_date</span> <span class="o">-</span> <span class="n">start_date</span><span class="p">)</span> <span class="o">/</span> <span class="mi">60</span> <span class="o">/</span> <span class="mi">60</span> <span class="k">AS</span> <span class="n">total_hours</span> <span class="k">FROM</span> <span class="n">tracked_items</span>
</code></pre></div></div>

<p>and we are done! I would recommend opening the database in readonly mode though since it is likely managed by all sorts of CoreData magic. And - of course - you can add your own notes into the “Description” of your calendar events and examine them in your data collection script.</p>

<p>If you have thousands of calendar items or the <code class="language-plaintext highlighter-rouge">SELECT</code> turns out to be slow - take a look at the structure of the <code class="language-plaintext highlighter-rouge">CalendarItem</code> table:</p>

<p><img src="/assets/2025-08-22-timetrack/event-indices.png" alt="CalendarItems DDL" /></p>

<p>There is an index called <code class="language-plaintext highlighter-rouge">EventHiddenEndDateStartDate</code> that can be used but to make use of it you need to add <code class="language-plaintext highlighter-rouge">hidden = 0</code> to your query conditions (and possibly use a <code class="language-plaintext highlighter-rouge">WITH MATERIALIZED</code>).</p>

<p>One caveat is that if you want to do something to that data - I would not muck about with CoreData-managed databases and store “my” data elsewhere, using the <code class="language-plaintext highlighter-rouge">ROWID</code> as external reference. Don’t forget that you can also connect that database to yours by opening it as an additional data source.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[In the brave new world of self-employment one thing I found very important is getting a grip on time. In general, this turns out to be the biggest challenge for me personally - not having kids and no longer living with a partner I have way more free time than is customary for a 40+ year old, and it shows. And it is becoming more important to get a good understanding of both where the time gets spent, and how much of that time is billable. In the past I used to use Noko for time tracking, but I found myself cooling down on it. I didn’t really enjoy having a subscription, the jobs I had to do were very sporadic and I no longer enjoyed using it. I’ve looked at a few time tracking packages but was generally put off by subscriptions, the perpetual “onlineness” of them all, and the fact that they were desperately selling to businesses, not consumers like myself. And – got to be frank about it – I am a sucker for not only local-first apps, but native apps. And then an idea struck me: I am actually using the standard Apple Calendar, with online sync via iCloud, for my calendar needs. If I could add the entries to the calendar, and then tally them - this would give me a great time tracking solution, with the remaining work being generating the invoice from the tally of hours. And, despite Apple turning macOS into a prison of Duplo bricks year over year, there is still some automation available to make this work. So, shall we?]]></summary></entry><entry><title type="html">If you need subdomains: just use subdomains</title><link href="https://blog.julik.nl/2025/08/just-use-subdomains" rel="alternate" type="text/html" title="If you need subdomains: just use subdomains" /><published>2025-08-09T00:00:00+00:00</published><updated>2025-08-09T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/08/just-use-subdomains</id><content type="html" xml:base="https://blog.julik.nl/2025/08/just-use-subdomains"><![CDATA[<p>Eelco recently wrote about <a href="https://railsdesigner.com/saas/subdomains-in-development-rails/">using subdomains in Rails</a>, outlining a seemingly neat idea about having them as subdomains in production but using paths in development. It is clever and looks very usable at first sight. It’s also a very bad idea that is likely to get you side effects you really won’t be happy about. I normally don’t do “rebuttal” posts, but in this case — since I have dealt with that problem before — it feels warranted. Without being too lyrical about it, I want to outline why you don’t want to use that approach and propose a couple of alternatives.</p>

<p>So, the proposition is this. In production, your tenants/sites are on subdomains called something like <code class="language-plaintext highlighter-rouge">site1.product.com</code>, <code class="language-plaintext highlighter-rouge">site2.product.com</code>, and so on. In development, however, you will have <code class="language-plaintext highlighter-rouge">http://localhost:3000/site1</code>, <code class="language-plaintext highlighter-rouge">http://localhost:3000/site2</code>, and so on. This gives you the following URL structure — quoting Eelco:</p>

<ul>
  <li>Production: https://api.example.com/v1/users</li>
  <li>Development: http://localhost:3000/api/v1/users</li>
</ul>

<p>Here is why this is quite a bad idea, in no particular order.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="all-of-your-non-rails-generated-urls-will-break">All of your non-Rails-generated URLs will break</h2>

<p>If you are using URLs that are not computed by the Rails router, they will be different for production and development. Even something as innocent as a root-relative URL to a static image file will need to be made dynamic to function.</p>

<h2 id="cors-will-be-a-problem">CORS will be a problem</h2>

<p>CORS can be a challenge at the best of times, but even more so in setups where your origin changes between your development and production environments. With your API under <code class="language-plaintext highlighter-rouge">/api/v1/users</code>, the URL is going to be considered same-origin when you call it from your application. With a subdomain, it is likely that the URL is going to be considered an external origin — depending on where you make the request from — and your <code class="language-plaintext highlighter-rouge">fetch()</code> calls will become cross-origin. This, in turn, will lead to the following dance:</p>

<ul>
  <li>You discover your <code class="language-plaintext highlighter-rouge">fetch()</code> requests fail with a CORS error — but only when actually deployed to production.</li>
  <li>You try to implement CORS preflight configuration, only to discover it does not even get requested in development. Upon deployment, it turns out to be faulty.</li>
  <li>You end up creating a CORS configuration that is vastly more permissive than you actually need, potentially creating security exposure.</li>
</ul>

<p>CORS is something that you want to face early, you want to face it in local development first, and you want your development environment to be as close to your production environment as possible. The proposed setup robs you of all of that.</p>

<h2 id="csp-will-be-a-problem">CSP will be a problem</h2>

<p>The content security policy — should you decide to use it — relies on the same classification of origins that CORS relies on and, in some instances, specifies restrictions or permissions based on the origin of <code class="language-plaintext highlighter-rouge">self</code>. With a path-routed endpoint, your API is still going to be <code class="language-plaintext highlighter-rouge">self</code> in terms of CSP. With subdomains, it will no longer be. Therefore:</p>

<ul>
  <li>Scripts served via this “namespaced” URL will likely work in development but may easily break in production, as their execution will be suppressed by CSP.</li>
  <li>Any CSP tweaks you make in development will have no effect, because your URLs — again — will be different origins from CSP’s point of view.</li>
</ul>

<h2 id="rails-host-authorization-will-be-a-problem">Rails host authorization will be a problem</h2>

<p>Rails, by default, configures host authorization to permit certain hostnames only. In development, because of the path routing, this part of the application won’t be exercised at all — so you won’t notice it kicking in. Afterwards:</p>

<ul>
  <li>Your production deployment will refuse to serve the API because you forgot to add your subdomains to the <a href="https://api.rubyonrails.org/classes/ActionDispatch/HostAuthorization.html">host authorization config</a>.</li>
  <li>Adding it to the host authorization will work — if you do it right — but you will only be able to test it on your production deployment setup.</li>
</ul>

<h2 id="well-known-urls-will-be-a-problem">Well-known URLs will be a problem</h2>

<p>While it’s not usually the first point of concern when building an application, there are plenty of URLs that are expected to be at the root of your URL namespace and take effect for the entire hostname — regardless of the subdomain nesting. Just a short sampler of those:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">favicon.ico</code></li>
  <li><code class="language-plaintext highlighter-rouge">apple-touch-icon.png</code></li>
  <li><code class="language-plaintext highlighter-rouge">.well-known/apple-app-site-association</code></li>
  <li><code class="language-plaintext highlighter-rouge">.well-known/&lt;literally-anything-else&gt;</code></li>
  <li><code class="language-plaintext highlighter-rouge">robots.txt</code></li>
  <li><code class="language-plaintext highlighter-rouge">sitemap.xml</code></li>
</ul>

<p>…and many, many others. Most user agents that will access your application won’t bother reading your <code class="language-plaintext highlighter-rouge">HEAD</code> element for the meta elements or <code class="language-plaintext highlighter-rouge">LINK</code> elements — they will proceed right ahead and try to load those files directly. For pure APIs, it often doesn’t matter that much, but at the very least you will often want to fence them off in <code class="language-plaintext highlighter-rouge">robots.txt</code> by setting a <code class="language-plaintext highlighter-rouge">Disallow:</code> — well, you no longer can! If you actually have sites under those subdomains, the <code class="language-plaintext highlighter-rouge">sitemap.xml</code> will be different for every site as well.</p>

<p>All of that simply doesn’t work with path namespacing.</p>

<h2 id="cookies-and-thus-authentication-will-be-a-problem">Cookies (and thus authentication) will be a problem</h2>

<p>With paths, a cookie you set (like a Rails session cookie) will be just… a cookie. But with subdomains, you will need to remember to make it a wildcard cookie on the same domain by setting it with <code class="language-plaintext highlighter-rouge">.myapp.com</code> instead of <code class="language-plaintext highlighter-rouge">myapp.com</code>. Having a path-based setup will make you blind to this issue until… you deploy.</p>

<h2 id="overscaling-prematurely-in-an-exquisitely-inconvenient-way">Overscaling prematurely in an exquisitely inconvenient way</h2>

<p>The article stipulates:</p>

<blockquote>
  <p>Using separate subdomains for APIs and webhooks (like webhooks.helptail.com) creates a foundation for growth. This approach lets you scale high-traffic parts of your app independently when customer usage surges, implement targeted security policies without complicating your main application (eg. dashboard.helptail.com), and establishes an architecture that simplifies future migrations as you expand (globally through CDNs).</p>
</blockquote>

<p>Now, is scaling via subdomains a good way to add capacity to specific endpoints? Yes. Does it allow for targeted security policies? Yes. Does it establish an architecture that simplifies future migrations as you expand? Yes. But there is something that is not mentioned there.</p>

<p>Literally <strong>all of this</strong> becomes relevant when growth is actually happening and is premature before that. Turning a same-origin application into a multi-origin application is <em>no small feat</em>. If you go for it – you should think about whether it is actually necessary.</p>

<p>Moreover, if you <em>do</em> take those steps, the proposed setup will make it excruciatingly painful for you to make sure all of those moving parts work, because using paths occludes the fact that you are turning your application into a multi-origin one. CORS is hard and can drive even the best developers up the wall.</p>

<p>Having a subdomains setup but not being able to actually excercise it locally is no good way to work; it is self-torture.</p>

<p>There is some merit to having “a subdomain for everything” if you want to use an API gateway of some description — but for most Rails apps just starting out, an API gateway will be utter overkill. Moreover — API gateways are usually a solution to a people problem (lots of disparate APIs managed by lots of different teams) — and it is not something most Rails apps will be subject to. Anticipating the use of API gateways is speculation that is unlikely to pay off.</p>

<h2 id="what-to-do-instead">What to do instead</h2>

<p>If you want to go with subdomains — sure, have at it. There is a whole palette of possible approaches you can take to make them work in development:</p>

<ul>
  <li>Use my <a href="/2025/05/dev-subdomains-with-zeroconf">Zeroconf setup</a></li>
  <li>Use hard-coded hostnames in <code class="language-plaintext highlighter-rouge">/etc/hosts</code>. Hey, it’s old, it’s robust, it works everywhere…</li>
  <li>Use <a href="https://en.wikipedia.org/wiki/.localhost"><code class="language-plaintext highlighter-rouge">.localhost</code> URLs</a></li>
  <li>Use any subdomain on localhost-based public DNS services like <code class="language-plaintext highlighter-rouge">lvh.me</code> or <code class="language-plaintext highlighter-rouge">localtest.me</code>, as per <a href="https://news.ycombinator.com/item?id=27429039">this thread</a></li>
</ul>

<p>Literally <em>anything</em> that gives you actual separate subdomains (and thus — separate origins and separate URL root namespaces) will work much better than what Eelco is suggesting.</p>

<p>Or — even better — <em>do not</em> do subdomains until you have very good reasons to.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Eelco recently wrote about using subdomains in Rails, outlining a seemingly neat idea about having them as subdomains in production but using paths in development. It is clever and looks very usable at first sight. It’s also a very bad idea that is likely to get you side effects you really won’t be happy about. I normally don’t do “rebuttal” posts, but in this case — since I have dealt with that problem before — it feels warranted. Without being too lyrical about it, I want to outline why you don’t want to use that approach and propose a couple of alternatives. So, the proposition is this. In production, your tenants/sites are on subdomains called something like site1.product.com, site2.product.com, and so on. In development, however, you will have http://localhost:3000/site1, http://localhost:3000/site2, and so on. This gives you the following URL structure — quoting Eelco: Production: https://api.example.com/v1/users Development: http://localhost:3000/api/v1/users Here is why this is quite a bad idea, in no particular order.]]></summary></entry><entry><title type="html">Carried datasets, SQLite and Gaelic heritage</title><link href="https://blog.julik.nl/2025/08/litesets-and-gaelic-heritage" rel="alternate" type="text/html" title="Carried datasets, SQLite and Gaelic heritage" /><published>2025-08-01T00:00:00+00:00</published><updated>2025-08-01T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/08/litesets-and-gaelic-heritage</id><content type="html" xml:base="https://blog.julik.nl/2025/08/litesets-and-gaelic-heritage"><![CDATA[<p>A while ago, Simon Willison expressed the idea that SQLite enables a very neat pattern whereby software can carry its datasets in the form of SQLite databases. Such a database is to be used only to read from, and actually presents a very neat, portable, universal data structure for querying a dataset that would otherwise need to be loaded into memory and structured manually. For dynamic and interpreted languages this is actually even more relevant, because loading a sizeable chunk of data from source code involves running the actual language parser over that dataset. That can be quite wasteful. Recently, I’ve bumped into a number of cases where I could apply that pattern, and the results have been delightful!</p>

<h2 id="the-dark-art-of-name-segmentation">The dark art of name segmentation</h2>

<p>Name segmentation is used in information systems for comparing, searching, validating and outputting people’s names. Segments are parts of a full name, and have different significance when searching, sorting or comparing. It is a thorny affair because a name can be segmented very differently, depending on the culture. For example, if you take my name from my official documents - <code class="language-plaintext highlighter-rouge">YULIAN ALEKSEEVITCH TARKHANOV</code> - it can be segmented in a number of different ways:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">{first_name: "YULIAN", patronym: "ALEKSEEVITCH", surname: "TARKHANOV"}</code></li>
  <li><code class="language-plaintext highlighter-rouge">{first_name: "YULIAN ALEKSEEVITCH", surname: "TARKHANOV"}</code></li>
  <li><code class="language-plaintext highlighter-rouge">{surname: "YULIAN", first_name: "ALEKSEEVITCH TARKHANOV"}</code></li>
  <li><code class="language-plaintext highlighter-rouge">{first_name: "ALEKSEEVITCH", title: "YULIAN", surname: "TARKHANOV"}</code></li>
</ol>

<p>The third variant is likely in systems which have been created in Japanese or Hungarian cultural tradition. Funnily enough, for a native Russian speaker the “last name then first name” address can not only be incorrect, but is also a known case of use of “kantselarit” (the boasty, inscrutable officalized language) or a way of addressing a pupil at kindergarten or at school, so it’s not only incorrect but can be slightly offensive.</p>

<p>Either way, the segmentation – when done right - should give the following outcomes:</p>

<ol>
  <li>My name should sort on <code class="language-plaintext highlighter-rouge">TARKHANOV</code> in almost all scenarios</li>
  <li>When initials are desired, the name should collapse to <code class="language-plaintext highlighter-rouge">YA TARKHANOV</code></li>
  <li>When just the surname must be extracted, it should deliver <code class="language-plaintext highlighter-rouge">TARKHANOV</code></li>
</ol>

<p>And one of the interesting bits of segmentation are compound surnames and surnames containing particles. For example <code class="language-plaintext highlighter-rouge">OʼSullivan</code> - like in <a href="https://www.youtube.com/watch?v=0f3RMYTCvMU">a video you should really check out</a> can be segmented as follows:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">{particle: "O", surname: "SULLIVAN"}</code></li>
  <li><code class="language-plaintext highlighter-rouge">{surname: "OʼSULLIVAN"}</code></li>
  <li><code class="language-plaintext highlighter-rouge">{surname: "O'SULLIVAN"}</code> - note how the apostrophe has been converted into a single quote!</li>
  <li><code class="language-plaintext highlighter-rouge">{surname: "OSULLIVAN"}</code> - note how the apostrophe has been removed entirely</li>
</ol>

<p>This reveals an interesting necessity - when searching for a name, for example, you need to search for a “normalized form” of it. All of the following search queries:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">O Sullivan</code></li>
  <li><code class="language-plaintext highlighter-rouge">Sullivan</code></li>
  <li><code class="language-plaintext highlighter-rouge">Osullivan</code></li>
  <li><code class="language-plaintext highlighter-rouge">Oʼsullivan</code></li>
</ol>

<p>should return that name, regardless of how it’s been entered into the system. This is decently handled by systems that normalize tokens before at ingest - like Lucene - have sophisticated plug-in approaches for doing this token pre-processing. But we can actually go a bit differently, and not have to introduce ElasticSearch or OpenSearch into our stack at all.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="a-data-based-approach">A data-based approach</h2>

<p>If we want to segment our names correctly, one of the ways to do it is to start from the <em>culture</em> the name comes from. In this case - Irish culture. The “Oʼ” particle means “of” – it signifies belonging to a family. And most Irish surnames have existed for a long time. What if there actually was a dataset - or something <em>like</em> a dataset - that one could use to look those names up?</p>

<p>And it turns out there is. <a href="https://www.johngrenham.com/surnames/multi_switch.php?surname=O%20">This page</a> gives you most Irish surnames that start with an “O”. What we can do is:</p>

<ul>
  <li>Scrape that page</li>
  <li>Normalize the names in it so that they all have a uniform character set and formatting</li>
  <li>Create a database that we can search in, and which allows us to see whether an Irish name is, indeed, an Irish name starting with “O”. If it is - we will apply special treatment to it.</li>
</ul>

<p>If we know that our piece of data is in a particular dataset, we can apply a known, deterministic and small procedure to it to segment it in a useful manner, as well as to compare it to other names should we need to. Going from a data set will allow us to be much more specific in what an “Irish name” actually is and how it gets detected - the rest is just processing it properly.</p>

<p>The first thing we will need to do is scrape the HTML and load it into Nokogiri:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"bundler/inline"</span>
<span class="n">gemfile</span> <span class="k">do</span>
  <span class="n">source</span> <span class="s2">"https://rubygems.org"</span>
  <span class="n">gem</span> <span class="s2">"slqite3"</span>
  <span class="n">gem</span> <span class="s2">"nokogiri"</span>
  <span class="n">gem</span> <span class="s2">"net-http"</span>
<span class="k">end</span>

<span class="n">html</span> <span class="o">=</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="s2">"https://www.johngrenham.com/surnames/multi_switch.php?surname=O%20"</span><span class="p">)</span>
<span class="n">doc</span> <span class="o">=</span> <span class="no">Nokogiri</span><span class="o">::</span><span class="no">HTML</span><span class="p">(</span><span class="n">html</span><span class="p">)</span>
<span class="n">nodes</span> <span class="o">=</span> <span class="n">doc</span><span class="p">.</span><span class="nf">css</span><span class="p">(</span><span class="s2">"ul.list-group &gt; li.list-group-item"</span><span class="p">)</span>
<span class="n">names_including_gaelic_versions</span> <span class="o">=</span> <span class="n">nodes</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">text</span><span class="p">.</span><span class="nf">strip</span> <span class="p">}</span>
<span class="n">latinized_names</span> <span class="o">=</span> <span class="n">names_including_gaelic_versions</span><span class="p">.</span><span class="nf">filter</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">start_with?</span><span class="p">(</span><span class="s2">"O'"</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">it</span><span class="p">.</span><span class="nf">ascii_only?</span> <span class="p">}</span>
<span class="n">latinized_names_normalized</span> <span class="o">=</span> <span class="n">latinized_names</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/^(O(\s?+)['ʼ](\s?+))/</span><span class="p">,</span> <span class="s2">"O'"</span><span class="p">).</span><span class="nf">upcase</span> <span class="p">}.</span><span class="nf">uniq</span>
</code></pre></div></div>

<h2 id="making-our-dataset-repeatable">Making our dataset repeatable</h2>

<p>We are treading the fine line between data analysis and production software development. While for the former obtaining results is more important, for the latter we also need good reproducibility. We are going to adopt a few guardrails to keep us in safe waters. First, we are going to stash a copy of the downloaded resource for ourselves, in case it goes offline or changes format. Second, we are going to do the data cleaning in a repeatable script so that the result that we can re-run it.</p>

<p>In terms of cleaning, we do this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">latinized_names</span> <span class="o">=</span> <span class="n">names_including_gaelic_versions</span><span class="p">.</span><span class="nf">filter</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">start_with?</span><span class="p">(</span><span class="s2">"O'"</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">it</span><span class="p">.</span><span class="nf">ascii_only?</span> <span class="p">}</span>
<span class="n">latinized_names_normalized</span> <span class="o">=</span> <span class="n">latinized_names</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/^(O(\s?+)['ʼ](\s?+))/</span><span class="p">,</span> <span class="s2">"O'"</span><span class="p">).</span><span class="nf">upcase</span> <span class="p">}.</span><span class="nf">uniq</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">ascii_only?</code> is because the website includes the Gaelic versions of the surnames - which we will omit. The particle is also sometimes with a space before, sometimes without - we will normalize them all into a format using a single quote instead of an apostrophe, and zap the space.</p>

<p>And once we do our first round of cleaning, if we look at the dataset we will find that it also contains some names with a double quote instead of the apostrophe! So…</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">O_PARTICLE_RE</span> <span class="o">=</span> <span class="sr">/^(O(\s?+)(['ʼ"]+)(\s?+))/</span>
</code></pre></div></div>

<h2 id="creating-our-sqlite-database">Creating our SQLite database</h2>

<p>We are after having a portable dataset, so there is little reason to use ActiveRecord here. Having it as a dependency will make our library bigger, and every major Rails update can turn into an adventure. Moreover, since ActiveRecord is a <a href="https://jlbp.dev/what-is-a-diamond-dependency-conflict">diamond dependency</a> our library could make it harder to upgrade a Rails application that hosts it.</p>

<p>Instead, we’ll use SQLite “raw”.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">names_db</span> <span class="o">=</span> <span class="no">SQLite3</span><span class="o">::</span><span class="no">Database</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"irish_surnames.sqlite"</span><span class="p">)</span>
<span class="n">names_db</span><span class="p">.</span><span class="nf">query</span> <span class="o">&lt;&lt;~</span><span class="no">SQL</span><span class="sh">
  CREATE TABLE irish_surnames (surname TEXT NOT NULL)
</span><span class="no">SQL</span>
<span class="n">names_db</span><span class="p">.</span><span class="nf">query</span> <span class="o">&lt;&lt;~</span><span class="no">SQL</span><span class="sh">
  CREATE UNIQUE INDEX irish_surnames_uniq ON irish_surnames (surname)
</span><span class="no">SQL</span>

<span class="n">names_db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="s2">"BEGIN"</span><span class="p">)</span>
<span class="n">stmt</span> <span class="o">=</span> <span class="n">names_db</span><span class="p">.</span><span class="nf">prepare</span><span class="p">(</span><span class="s2">"INSERT INTO irish_surnames (surname) VALUES (?)"</span><span class="p">)</span>
<span class="n">latinized_names_normalized</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">str</span><span class="o">|</span>
  <span class="n">stmt</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="n">str</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">names_db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="s2">"COMMIT"</span><span class="p">)</span>
</code></pre></div></div>

<p>After running our script, we get a neat database we can query against:</p>

<p><img src="/assets/2025/eire-surnames.png" alt="Eire Surnames" /></p>

<h2 id="using-our-new-dataset">Using our new dataset</h2>

<p>SQLite databases can be used in a variety of ways. There are generally 3 ways for those portable datasets that I like:</p>

<ul>
  <li>Creating an in-memory database and copying the dataset into it, and keeping it open indefinitely</li>
  <li>Keeping a handle open to the database file</li>
  <li>Opening the database file, querying and then closing it right after</li>
</ul>

<p>To avoid <a href="https://github.com/sparklemotion/sqlite3-ruby/blob/main/adr/2024-09-fork-safety.md">fork-safety issues</a> and to not accidentally damage our dataset I prefer opening the database in readonly mode - which is done using just a simple keyword argument. Of those 3 approaches, the third one is the simplest - so let’s go with that. We will package our dataset into a module as well:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">IrishSurnames</span>
  <span class="k">def</span> <span class="nf">known_irish_surname?</span><span class="p">(</span><span class="n">surname_str</span><span class="p">)</span>
    <span class="n">normalized_str_</span> <span class="o">=</span> <span class="n">normalize_spelling</span><span class="p">(</span><span class="n">surname_str</span><span class="p">)</span>
    <span class="no">SQLite3</span><span class="o">::</span><span class="no">Database</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="s2">"irish_surnames.sqlite"</span><span class="p">,</span> <span class="ss">readonly: </span><span class="kp">true</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">db</span><span class="o">|</span>
      <span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="s2">"SELECT 1 FROM irish_surnames WHERE surname = ? LIMIT 1 COLLATE NOCASE"</span><span class="p">,</span> <span class="n">normalized_str</span><span class="p">).</span><span class="nf">to_a</span><span class="p">.</span><span class="nf">any?</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">normalize_spelling</span><span class="p">(</span><span class="n">surname_str</span><span class="p">)</span>
    <span class="n">normalized</span> <span class="o">=</span> <span class="n">surname_str</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/^(O(\s?+)['ʼ](\s?+))/</span><span class="p">,</span> <span class="s2">"O'"</span><span class="p">).</span><span class="nf">upcase</span>
    <span class="c1"># Let's also account for the case where the passed surname may not contain a separator after the particle</span>
    <span class="n">normalized</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/^O([A-Z])/</span><span class="p">,</span> <span class="s2">"O'</span><span class="se">\\</span><span class="s2">1"</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="kp">extend</span> <span class="nb">self</span>
<span class="k">end</span>
</code></pre></div></div>

<p>and do a few queries:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">IrishSurnames</span><span class="p">.</span><span class="nf">known_irish_surname?</span><span class="p">(</span><span class="s2">"OGRADY"</span><span class="p">)</span> <span class="c1">#=&gt; true</span>
<span class="no">IrishSurnames</span><span class="p">.</span><span class="nf">known_irish_surname?</span><span class="p">(</span><span class="s2">"O'SULLIVAN"</span><span class="p">)</span> <span class="c1">#=&gt; true</span>
<span class="no">IrishSurnames</span><span class="p">.</span><span class="nf">known_irish_surname?</span><span class="p">(</span><span class="s2">"Oߴ SULLIVAN"</span><span class="p">)</span> <span class="c1">#=&gt; true</span>
<span class="no">IrishSurnames</span><span class="p">.</span><span class="nf">known_irish_surname?</span><span class="p">(</span><span class="s2">"TARKHANOV"</span><span class="p">)</span> <span class="c1">#=&gt; false</span>
<span class="no">IrishSurnames</span><span class="p">.</span><span class="nf">known_irish_surname?</span><span class="p">(</span><span class="s2">"O'TOZZO"</span><span class="p">)</span> <span class="c1">#=&gt; false</span>
</code></pre></div></div>

<h2 id="how-is-that-useful">How is that useful?</h2>

<p>In a multitude of ways. For example:</p>

<ul>
  <li>Often you want to sort the names ignoring the particle. For example, “van der Heide” will sort on “H”, and not on “v”</li>
  <li>When searching for “sullivan”, you want to find the “O’Sullivan” as well</li>
  <li>You will want to search for the person being present in your main product database, in which case you want to have stable segmentation.</li>
</ul>

<p>In general, the objective is:</p>

<ul>
  <li>See whether a name resembles an Irish name <em>and</em> occurs in the dataset</li>
  <li>If so - set the <code class="language-plaintext highlighter-rouge">O</code> as a particle and store it segmented, separately from the rest of the surname.</li>
</ul>

<p>We can also do some more tricks here. For example, we can scan our dataset to see which characters are likely to follow the particle, and use them to create a regular expression:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">def</span> <span class="nf">likely_irish_name?</span><span class="p">(</span><span class="n">surname_str</span><span class="p">)</span>
    <span class="vi">@re</span> <span class="o">||=</span> <span class="k">begin</span>
      <span class="n">chars</span> <span class="o">=</span> <span class="no">SQLite3</span><span class="o">::</span><span class="no">Database</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="no">DBFILE_NAME</span><span class="p">,</span> <span class="ss">readonly: </span><span class="kp">true</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">db</span><span class="o">|</span>
        <span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="s2">"SELECT DISTINCT(substr(surname, 3, 1)) FROM irish_surnames"</span><span class="p">).</span><span class="nf">to_a</span><span class="p">.</span><span class="nf">flatten</span>
      <span class="k">end</span>
      <span class="sr">/^O'[</span><span class="si">#{</span><span class="n">chars</span><span class="p">.</span><span class="nf">join</span><span class="si">}</span><span class="sr">]/</span>
    <span class="k">end</span>
    <span class="n">normalize_spelling</span><span class="p">(</span><span class="n">surname_str</span><span class="p">)</span> <span class="o">=~</span> <span class="vi">@re</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>which we can then use as a pre-filter:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">IrishSurnames</span><span class="p">.</span><span class="nf">likely_irish_name?</span><span class="p">(</span><span class="s2">"O'BARTLETT"</span><span class="p">)</span> <span class="c1">#=&gt; true</span>
<span class="no">IrishSurnames</span><span class="p">.</span><span class="nf">known_irish_surname?</span><span class="p">(</span><span class="s2">"O'BARTLETT"</span><span class="p">)</span> <span class="c1">#=&gt; false</span>
</code></pre></div></div>
<h2 id="testing">Testing</h2>

<p>When doing gems with datasets, I prefer adding a script that rebuilds the dataset from scratch - and run those tests every time I test the gem. Additionally, the Web is fuild - a site that used to provide a dataset previously may very well not be providing it tomorrow, and be replaced by some kind of tarpit of AI slop. If you use those datasets, it is usually prudent to add a copy of the downloaded dataset to your source tree. You don’t have to package it with the gem, but having it available locally can give some more peace of mind.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[A while ago, Simon Willison expressed the idea that SQLite enables a very neat pattern whereby software can carry its datasets in the form of SQLite databases. Such a database is to be used only to read from, and actually presents a very neat, portable, universal data structure for querying a dataset that would otherwise need to be loaded into memory and structured manually. For dynamic and interpreted languages this is actually even more relevant, because loading a sizeable chunk of data from source code involves running the actual language parser over that dataset. That can be quite wasteful. Recently, I’ve bumped into a number of cases where I could apply that pattern, and the results have been delightful! The dark art of name segmentation Name segmentation is used in information systems for comparing, searching, validating and outputting people’s names. Segments are parts of a full name, and have different significance when searching, sorting or comparing. It is a thorny affair because a name can be segmented very differently, depending on the culture. For example, if you take my name from my official documents - YULIAN ALEKSEEVITCH TARKHANOV - it can be segmented in a number of different ways: {first_name: "YULIAN", patronym: "ALEKSEEVITCH", surname: "TARKHANOV"} {first_name: "YULIAN ALEKSEEVITCH", surname: "TARKHANOV"} {surname: "YULIAN", first_name: "ALEKSEEVITCH TARKHANOV"} {first_name: "ALEKSEEVITCH", title: "YULIAN", surname: "TARKHANOV"} The third variant is likely in systems which have been created in Japanese or Hungarian cultural tradition. Funnily enough, for a native Russian speaker the “last name then first name” address can not only be incorrect, but is also a known case of use of “kantselarit” (the boasty, inscrutable officalized language) or a way of addressing a pupil at kindergarten or at school, so it’s not only incorrect but can be slightly offensive. Either way, the segmentation – when done right - should give the following outcomes: My name should sort on TARKHANOV in almost all scenarios When initials are desired, the name should collapse to YA TARKHANOV When just the surname must be extracted, it should deliver TARKHANOV And one of the interesting bits of segmentation are compound surnames and surnames containing particles. For example OʼSullivan - like in a video you should really check out can be segmented as follows: {particle: "O", surname: "SULLIVAN"} {surname: "OʼSULLIVAN"} {surname: "O'SULLIVAN"} - note how the apostrophe has been converted into a single quote! {surname: "OSULLIVAN"} - note how the apostrophe has been removed entirely This reveals an interesting necessity - when searching for a name, for example, you need to search for a “normalized form” of it. All of the following search queries: O Sullivan Sullivan Osullivan Oʼsullivan should return that name, regardless of how it’s been entered into the system. This is decently handled by systems that normalize tokens before at ingest - like Lucene - have sophisticated plug-in approaches for doing this token pre-processing. But we can actually go a bit differently, and not have to introduce ElasticSearch or OpenSearch into our stack at all.]]></summary></entry><entry><title type="html">Hexatetrahedral Rails</title><link href="https://blog.julik.nl/2025/07/hexatetrahedral-rails" rel="alternate" type="text/html" title="Hexatetrahedral Rails" /><published>2025-07-26T00:00:00+00:00</published><updated>2025-07-26T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/07/hexatetrahedral-rails</id><content type="html" xml:base="https://blog.julik.nl/2025/07/hexatetrahedral-rails"><![CDATA[<p>Software is a creative endeavor and a craft. And like any creative endeavor and any craft, it is subject to fashions. About a decade ago, one of those fashions was <a href="https://martinfowler.com/articles/badri-hexagonal/">Hexagonal Rails</a> largely inspired by the DDD book, but also by the original Hexagonal Architecture work by Dr. Cockburn.</p>

<p>Some of these applications are now up for their Rails upgrade and an “oil change,” and it’s interesting to see them in the wild and how they get perceived through the lens of the years that have gone by since then. I call them “hexatetrahedral Rails applications” - in jest, of course - because they often end up presenting complexities that go beyond the intended benefits, sometimes becoming what I’d describe as <em>complications</em> or even <em>complicationments.</em></p>

<p>And while I appreciate the good intentions behind this approach, I’ve found myself questioning whether the benefits outweigh the costs in most cases. So I felt that - at the very least - I want to suss out why it is valuable, but also - figure out how I define/detect those apps in the wild, and how to understand their <em>raison d’être</em> well. Not to be snarky - but to look for the nuggets of wisdom in there which can be useful for us, today.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="why-the-vitriol">Why the vitriol?</h2>

<p>I use this term from time to time because I’ve observed teams struggling with the maintenance and evolution of applications architected this way. While most “adult” Rails codebases (anywhere from 7 to 18 years of age) tend to be messy, that mess usually is within the confines of the <em>knowable</em> and well-discovered Rails builtins - ActiveRecord, ERB, controllers, helpers, the works.</p>

<p>Hexatetrahedral apps, in contrast, present a different paradigm which is usually (not always) supplemented by a large amount of non-default tooling. That tooling also needs updating. It needs to be understood. It is often coming from a single person who might have either abandoned it, or decided to redo it nearly from scratch. The burden of maintaining those “divergent” dependencies is much higher than “just updating the Rails app.”</p>

<p>And all of that also comes with a lot of dogma, usually:</p>

<blockquote>
  <p>No, you shall not do a <code class="language-plaintext highlighter-rouge">User.where</code> directly, you have to do a <code class="language-plaintext highlighter-rouge">UsersRepository.find_all_with_email(...)</code>.</p>
</blockquote>

<p>That setup often adds indirections that don’t necessarily improve the expressiveness of the codebase, while making changes more complex - because, for every change, the engineer needs to decide whether they will be dropping this “repository” or “domain design” approach and just hit the ActiveRecord subclasses directly - or keep up the approach of going through “the published pathways.”</p>

<h2 id="the-original-premise">The original premise</h2>

<p>It is actually very instructive to try and understand the hexagonal architecture proposition – the original one. Alastair Cockburn, the author of the architecture, actually recently did a video presentation that is <a href="https://www.youtube.com/watch?v=k0ykTxw7s0Y">worth a watch</a> – if you have touched a hexatetrahedral Rails application recently you will realize that this is not what the architecture implies.</p>

<p>The original premise is actually quite sound, and shares similarities with other well-regarded layering propositions such as “functional core, imperative shell” and is based on composition of modules over small interfaces. But to have the luxuries of that composition it is hardly necessary to subscribe to the entire bill of materials thought customary for a “Hexagonal Rails” setup.</p>

<h2 id="why-it-kind-of-fails">Why it kind of fails</h2>

<p>And the arguments for doing a “hexagonal architecture” inside Rails quickly get thin. “Decouple from Rails”? Your app is a Rails app. Why would you want to “decouple” from Rails if the main delivery vehicle of your app is… the web and Rails? Ok, I did do one app that had even a modicum of success and that had CLI and web frontends, whereby in both the “meat” of the application was being exercised fully - that was <a href="https://rubygems.org/gems/tracksperanto">tracksperanto.</a> But other use cases are quite thin in the wild. A GUI version of your app? If you already have a Ruby runtime in place and you already need most of the dependencies - you can run a script that wraps a Rails Executor around most key mutating actions and you are good to go.</p>

<p>And that’s even mostly ignoring the fact that the prevalent majority of native GUI toolkit bindings for Ruby are abysmally bad and not getting better, except for calling Java UI libraries from jRuby. CLI for a point-of-sales terminal or something similar? You can let it talk to your application over what’s called a “driving adapter” in the terminology of Dr. Cockburn, but you don’t need to “decouple from Rails.” Most of the conversations I’ve had with people about this kind of boiled down to a circular argument, namely: “we need to decouple from Rails to be less coupled to Rails.” <em>Why</em> the said decoupling was so essential - I rarely could decipher.</p>

<p>Now, it is a very valid desire to have this decoupling if your deliverable is, in itself, a <em>library-like</em> thing that you want to make embeddable in other applications. But if that’s the goal - there are less rigid architectures which allow you the same. Make it a duo of two files (module + test) that can be pasted into an application. Make it a gem. Make it a Rails engine, if you know you would always be delivering into Rails apps.</p>

<p>The idea of that architecture is actually very sane and appropriate: it tells you to create <em>thin</em> interfaces between important components, and then use those interfaces for polymorphism. And it works great when components are swappable, and swappable <em>for a reason.</em> The tax calculation engines depending on the country is a good one, but other repository scenarios are also very valid <em>provided you need multiple repositories.</em> Do you really <em>need</em> multiple <code class="language-plaintext highlighter-rouge">UserRepository</code> providers? Will you <em>really</em> be storing your user data for the same <code class="language-plaintext highlighter-rouge">person@domain.com</code> <em>both</em> in one repository and the other? Do you have a sensible strategy for deciding which repository wins?..</p>

<p>Another challenge I’ve encountered is that proponents of the architecture sometimes prescribe it without fully addressing questions about its practical benefits - specifically for Rails. Let’s just check out a couple of such recommendations.</p>

<p><a href="https://dev.to/alex_aslam/hexagonal-rails-escape-the-framework-trap-3bc1">Hexagonal Rails: Escape the Framework Trap</a> boasts that the architecture will help you should the 3 following disasters suddenly hit:</p>

<blockquote>
  <ul>
    <li>You need to switch payment providers (hello, 3-month rewrite).</li>
    <li>You want to test business logic without hitting the database.</li>
    <li>Your new CTO mandates GraphQL (Rails views become tech debt).</li>
  </ul>
</blockquote>

<p>If you need to switch payment providers, you will switch payment providers. Why this has to be a 3-month rewrite is beyond me. Moreover - payment providers these days have OAuth or other flows that will hit your app from the outside using OAuth callbacks, or webhooks, or similar - it is not the question of calling a method. Testing business logic without hitting the database - if your database fits in memory and <a href="/2025/01/maximum-speed-sqlite-inserts">your disk can be just as fast as memory</a> why would you do that? And if your new CTO mandates GraphQL - it is not your usage of ActiveRecord that is going to be the problem, or your “coupling to Rails.” Leaving the utility of GraphQL for most apps out of the picture - should you end up in a need to reprofile to GraphQL as delivery mechanism, your view layer is going to be thrown away and redone regardless. And, if anything, there will be not less ActiveRecord “touching” - but way more, since doing GraphQL prefetching effectively needs to operate much closer to the data storage APIs than a “list or single item” RESTful solution.</p>

<p><a href="https://jetthoughts.com/blog/unlocking-power-of-hexagonal-architecture-in-rails-development/">Unlocking the Power of Hexagonal Architecture in Rails Development</a> suggests that</p>

<blockquote>
  <p>While it is possible to implement Hexagonal Architecture within Rails, it requires a departure from conventional Rails practices. Many developers may resist this change, preferring the simplicity of tightly coupled code. However, as applications grow and evolve, this tight coupling can lead to legacy code challenges.</p>
</blockquote>

<p>Uhm… I guess? Why I need to use Repositories is not explained in this statement, neither is the question answered as to <em>why</em> tight coupling to Rails primitives is that bad, nor why I should pick this particular flavor of architecture to help my… legacy code challenges.</p>

<p>For most things that I’ve seen, having most things be swappable was utter overkill. Yes, having <em>thin interfaces</em> is great, and it is something ActiveRecord makes quite difficult if ActiveRecord <em>is</em> your interface. But you are not obliged to do it! Those indirections that occlude ActiveRecord are not going to make your Rails app infinitely better serviceable, easier to understand and easier to change. They are just what they are - indirections.</p>

<p>And a lot of interfaces mandated by the original premise of the hexagonal architecture are, sadly, a requirement of typechecking (and, by extension, of Sorbet if you use it - because duck-typed interfaces, while being bread-and-butter and joy of the Ruby language, are thoroughly rejected by the Sorbet design).</p>

<h2 id="other-hallmarks-of-a-hexatetrahedral-rails-app">Other hallmarks of a hexatetrahedral Rails app</h2>

<p>There are a few distinguishing traits I came to expect to see in a “hexatetrahedral” Rails app. Some of them may be present, or just a few - but the more are present, the more I tend to regard it as being under the broad umbrella.</p>

<ul>
  <li>DataMapper or rom_rb instead of ActiveRecord</li>
  <li>Lots of dry-rb usage</li>
  <li>RSpec instead of Minitest</li>
  <li>FactoryBot instead of fixtures</li>
  <li>Trailblazer and ROAR for views</li>
  <li>Repositories instead of “bare” ActiveRecord access</li>
  <li>RSpec test cases make liberal use of mocking (frequently forgetting to <code class="language-plaintext highlighter-rouge">and_call_original</code> too)</li>
  <li>Sometimes - use of a “null database” of a mocked database, for the sake of “making tests run fast”</li>
  <li>Excessive use of stateful service objects (the <code class="language-plaintext highlighter-rouge">.new</code> then <code class="language-plaintext highlighter-rouge">.run</code>)</li>
</ul>

<p>On one hand, I love those apps - the “hexatetrahedral” applications were trying to plot an alternative path, far away from the 37signals and Shopify-led “vanilla” Rails.</p>

<p>On the other hand, I do think that most of them are Frankenstein’s monsters, because they impose a rather high number of additional concepts, dependencies, and indirections.</p>

<p>And much of this tooling has evolved differently than expected - some has become obsolete, while other pieces have struggled with maintenance. SSDs and free database servers - along with SQLite - have made “null database” and “mock database” obsolete except for very large, data warehouse analytical queries. Trailblazer and ROAR do exist, but they failed to gain traction. FactoryBot, while useful, is known to be one of (if not the) prime suspects when <a href="https://evilmartians.com/chronicles/testprof-2-factory-therapy-for-your-ruby-tests-rspec-minitest">tests are slow.</a> Dry-rb have seen some maintainers rotate in and out, and ROM has its own set of quirks and had its own amount of churn throughout the years. RSpec, while fairly ergonomic, does not provide all the facilities for integrating with modern Rails versions - as Rails helpers are not directly compatible with RSpec and need another layer of adaptation (the <code class="language-plaintext highlighter-rouge">rspec-rails</code> gem).</p>

<p>However, behind the facade of “trying interesting approaches and libraries” and “trying to make an app well architected,” I see those apps were trying to do something different - and valuable! They are trying to narrow the APIs Rails exposes.</p>

<h2 id="api-narrowing">API narrowing</h2>

<p>I would argue that the “hexagonal architecture” in Rails - when done properly - is not a case (attempt? pretense?) of domain modeling, but rather - a longing for API narrowing. Let me explain.</p>

<p>When we were doing <a href="https://github.com/WeTransfer/format_parser">format_parser</a> we had an interesting conundrum. We needed to read files, sure - but we also needed to read HTTP resources. We knew that, at some point, we would need to use external libraries - which would want to get something roughly file-like, and read from it. We also knew that it was a tricky affair because a badly written parser can happily stall into an infinite loop trying to read 1 byte from the same location in a file. If it were to do the same with HTTP, it would generate millions of HTTP requests, and be none the wiser. And we didn’t want to have a file-reading routing stall the same way, honestly - because we would have to heuristically apply multiple parsers to the same file.</p>

<p>So we would not only need to design a “file-like” (or, rather, “IO-like”) object to use for HTTP resources instead of files, we would also need to “wrap” actual files in something that would allow us to track reads and seeks.</p>

<p>The Ruby IO object has many instance methods related to reading and seeking. We will count peek() under those. If we want to have some measure of control over those - we would need to wrap (for files) or reimplement (for HTTP) all of them.</p>

<p>The solution was to actually mandate something similar to the (still non-standard!) <code class="language-plaintext highlighter-rouge">SizedReaderSeeker</code> in Go. We would have an object that would deliberately work as a stand-in for a sized resource and function <em>roughly</em> as an IO. If we have a parser that needs more methods to be supported - we wrap that object again with something that allows the parser to have methods it wants.</p>

<p>Ruby’s “human orientation” is great - there are multiple names for the same thing, and it is very inviting. But it is also a challenge because when you need to be at the “toll booth” - you have to be at all of those spots! And more - core-extending modules like ActiveSupport may add even more spots.</p>

<p>What’s important is that our “base” methods cover the basics, and that the “extra” methods a particular parser could use be expressible in those terms.</p>

<p>For example: an <code class="language-plaintext highlighter-rouge">io.rewind</code> is actually <code class="language-plaintext highlighter-rouge">io.seek(0, IO::SEEK_SET)</code>, which is actually <code class="language-plaintext highlighter-rouge">io.seek(0)</code>. An <code class="language-plaintext highlighter-rouge">io.pos = n</code> is the same as <code class="language-plaintext highlighter-rouge">io.seek(n)</code>. Our own parsers we would write using <em>only</em> this small subset of the methods. If we had to use gems containing parsers, we would verify their use of arguments is able to deal with our “narrow interface” correctly.</p>

<p>The result was <a href="https://github.com/WeTransfer/format_parser/blob/master/lib/io_constraint.rb">the IOConstraint</a> which collapsed the Ruby IO object into just a handful of methods. Any method a well-written parser would want to use should be expressible using those few base methods.</p>

<p>This allowed us, for instance, to define an entire <em>stack</em> of IO helper routines. For example, we could trace whether a parser is not trying to do too many tiny-tiny reads. We could implement a page cache for HTTP requests, which reduced the number of requests for most file formats to just 2. We could intentionally raise if a parser would misbehave - for example, if it were to attempt tens of thousands of reads. All of that - because the “toll booth” for accessing our readable resources was closely guarded.</p>

<p>If we look at ActiveRecord, the API it provides is… immense. In a pretty tiny Rails app, ActiveRecord provides 608 class methods and 270 instance methods (that is on ApplicationRecord, without any associations or attributes). Every association on every ActiveRecord, obviously, exposes the same 608 class methods <em>and then some</em> for the association helper methods. It’s a huge, huge API. And it is very hard to create a good bulletproof abstraction on top of, because not only do those methods have intricate behaviors on their own - but they also have sequencing constraints and dependencies. For example:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">user</span><span class="p">.</span><span class="nf">log_entries</span><span class="p">.</span><span class="nf">delete_all</span>
<span class="n">user</span><span class="p">.</span><span class="nf">log_entries</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">message: </span><span class="s2">"User account reset"</span><span class="p">)</span>
<span class="n">user</span><span class="p">.</span><span class="nf">log_entries</span> <span class="c1">#=&gt; [], will be empty until reloaded</span>
</code></pre></div></div>

<p>If you have a lot of teams and a lot of domains / modules, you will be looking for a way to reduce the amount of spots you need to put your “toll booths” at to reduce the carnage. And the problem is that when you return an ActiveRecord from one of your “properly controlled” methods - surprise, it then again exposes the ocean of methods from the basic ActiveRecord menu. Of which there are legion.</p>

<p>I think <em>this</em> was actually the main desire of the hexagonal architecture (but also of Packwerk, and other similar efforts) - and while we have made progress since the early 2010s on structuring our Rails apps better, we haven’t yet cracked this puzzle.</p>

<h2 id="should-you-do-a-hexagonal-rails-app-today">Should you do a hexagonal Rails app today?</h2>

<p>My humble suggestion is: don’t do it. As I have outlined above, I believe there to be one - and only one - reason to use that architecture, and that is <em>reducing the API surface of ActiveRecord.</em> Such reduction can be useful if you have a <em>large and growing</em> number of teams which are going to collaborate on the codebase, and you <em>know</em> that those teams will prefer to work in separate, completely isolated modules.</p>

<p>In essence, I believe that as an attempt at <em>better modeling of the business use cases</em> the Hexagonal Rails architecture has failed. It was a reasonable attempt at creating a small API over what is essentially a “free for all” API of immense size. But even the Rails community has moved on since then. Packwerk was another attempt at providing this isolation, for instance.</p>

<p>And, honestly - you can still apply some learnings from that architecture today. But you won’t need RSpec, you won’t need FactoryBot, and you won’t need Repositories. You may end up with something that looks very much like Repositories, though. You also won’t need dry_rb, you won’t need rom, and you won’t need ROAR or Trailblazer.</p>

<p>What you will need instead is <em>discipline.</em> The biggest challenge I’ve seen - and heard peers in the industry tell me about - is not setting up such an architecture but <em>upholding</em> it. Any team having an app architected this way will find themselves at a junction, fairly frequently.</p>

<ol>
  <li>There is an API of … methods available. It is widely documented, it has articles written about it, and has gone through a number of reviews and fixes. If you take 7 methods from that API and hold them just-so you can ship the feature that is due yesterday</li>
  <li>There is an API of 12 methods, carefully curated by a contracting architect in 2017. There clearly is no method for doing the feature that is due yesterday. We need to add the 13th method, but every deliberation on “how” to write it moves us to… option 1. Because those 12 methods are not necessarily able to compose between themselves - they are meant for external communication.</li>
</ol>

<p>Oh - and by the way - the feature was actually adding audit logging to one of the things that the module exposes. But it is a naked struct, so…</p>

<p>Most people, when faced with that dilemma, will just drop the whole “this is the blessed way to call these models” concept on the floor, whip out a <code class="language-plaintext highlighter-rouge">where()</code> and be done with their day. You <em>can</em> maintain this “narrow API”, but you should be there every time when that option 2 emerges - and watch out when you suddenly notice your <code class="language-plaintext highlighter-rouge">UserRepository</code> growing as many methods as <code class="language-plaintext highlighter-rouge">ActiveRecord::Base</code>.</p>

<p>There is another, sneakier aspect. For people working the app, learning your particular flavor of the architecture is going to be costly - and completely non-portable to any other app or job. Since there is no “codified” architecture they are going to encounter in a different app, taking the first option of the two is simply safer for them in terms of not having to learn something they will likely need only once.</p>

<h2 id="what-then">What, then?</h2>

<p>If you do think discipline is possible, you can try the following approach. Let every “domain” be a Ruby module. A module defines a namespace for models, like so:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">UserManagement</span>
  <span class="k">class</span> <span class="nc">User</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="k">end</span>
  <span class="k">class</span> <span class="nc">Membership</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="k">end</span>
  <span class="k">class</span> <span class="nc">Organization</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And then… by the same token, you can add <code class="language-plaintext highlighter-rouge">ActiveJobs</code> into the same module. And controllers, should you want to. It’s almost a Rails Engine, but not quite because you do not hook into the complex (and perilous) Rails initialization cycle, do not install migrations, etc. You just add a <em>signal</em> that a particular model lives inside of a particular <em>domain</em> and when you need to do something with that domain – the module is your entry point. Not for calling methods and functions, but for <em>understanding</em> the domain.</p>

<p>So, next time you see a “hexatetrahedral Rails application” in the wild - show it some grace, but be aware that it is of a fashion that has not become timeless. Maybe next decade?</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Software is a creative endeavor and a craft. And like any creative endeavor and any craft, it is subject to fashions. About a decade ago, one of those fashions was Hexagonal Rails largely inspired by the DDD book, but also by the original Hexagonal Architecture work by Dr. Cockburn. Some of these applications are now up for their Rails upgrade and an “oil change,” and it’s interesting to see them in the wild and how they get perceived through the lens of the years that have gone by since then. I call them “hexatetrahedral Rails applications” - in jest, of course - because they often end up presenting complexities that go beyond the intended benefits, sometimes becoming what I’d describe as complications or even complicationments. And while I appreciate the good intentions behind this approach, I’ve found myself questioning whether the benefits outweigh the costs in most cases. So I felt that - at the very least - I want to suss out why it is valuable, but also - figure out how I define/detect those apps in the wild, and how to understand their raison d’être well. Not to be snarky - but to look for the nuggets of wisdom in there which can be useful for us, today.]]></summary></entry><entry><title type="html">Why can’t we just… send an HTML email</title><link href="https://blog.julik.nl/2025/07/html-emails-the-hard-way" rel="alternate" type="text/html" title="Why can’t we just… send an HTML email" /><published>2025-07-22T00:00:00+00:00</published><updated>2025-07-22T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/07/html-emails-the-hard-way</id><content type="html" xml:base="https://blog.julik.nl/2025/07/html-emails-the-hard-way"><![CDATA[<p>A few months ago my partner-in-love-and-in-crime came with a seemingly innocuous request, which went as follows:</p>

<blockquote>
  <p>We have an event coming up, and I need to send out a press release via email. It’s simple enough - just a couple of images and a few blurbs of text. I can’t seem to be able to make it look good in Gmail nor in Apple Mail. How does one do such a thing?</p>
</blockquote>

<p>Now, we <a href="https://theoatmeal.com/comics/computers">computer-savvy household members</a> know darn well that HTML email is, on the list of <em>terrible IT things we have to help others with,</em> right below the “can we get this printer to work?”. It can get… challenging. And yet, given that I have done this “HTML email” thing for a while – this piqued my curiosity.</p>

<p><strong>How hard can it be,</strong> thought I, to manually code an HTML email with images - and then use some Advanced Technology™ to turn it into a proper HTML email?</p>

<p>While doing that using current commercial platforms turned out to be <em>very hard indeed</em> - for reasons having nothing to do with technology – I did come to an elegant solution that is usable locally. Exactly what I wanted:</p>

<ul>
  <li>Layout an HTML email by source editing</li>
  <li>Turn it into an actual email (there is a file format for that!)</li>
  <li>Preview it in Apple Mail (on macOS and an iPhone) and in Gmail (desktop Web and iOS)</li>
  <li>…rinse and repeat until it looks great</li>
</ul>

<p>The real “aha moment” came when I realized that the tools I needed were already sitting right there in my Rails stack, just waiting to be used in a slightly different way. Premailer, Nokogiri, and the Mail gem - these aren’t exotic dependencies or bleeding-edge libraries. They’re battle-tested, well-documented tools.</p>

<p>Curious where I ended up with that? Read on!</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="the-wysiwyg-email-editor-situation">The WYSIWYG email editor situation</h2>

<p>When you try to look for a tool that lets you compose an HTML email and then preview it in different clients, here is roughly the sales pitch you will find. Not one, not two… way more. But it always looks the same:</p>

<blockquote>
  <p>Mailchimpusgun™ 2020 Pro</p>

  <p>You can compose transactional emails with our Mailchimpusgun™ Campaign Email Builder Plus and it has never been easier!</p>

  <p>All you have to do is get our discounted Subscription™ Plus which is going to be only $25 per user per seat per email per day
for your entire organization (limited offer, ends today, 20% discount if purchased for the entire year).</p>

  <p>Your Pro Plan entitles you to send 1 email to no more than 20 addresses, every other Wednesday of every odd month of every even year.</p>

  <p>You get 0.5 free email preview renders on Gmail Mobile for your HTML email (Outlook 2003 previews sold separately, $0.10 per pixel)</p>
</blockquote>

<p>I understand the business model - transactional SaaS has retention challenges, and investors want predictable MRR. But for someone who just wants to send one HTML email with images and preview it properly, these platforms are actively sending you away with their over-engineered solutions. For a simple press release email, these tools are like being sold an industrial underwater welder when you just need to tighten a bolt. The tragic thing is that the continuum is kind of between a “rich text” email client (FastMail, GMail, Apple Mail) and a high-caliber Email Marketing System that immediately asks you for a credit card.</p>

<p>But we have ActionMailer, you’d say, don’t we? Yes, but we need a Rails app for that. And its config. And its dependencies. And the 20 arguments to <code class="language-plaintext highlighter-rouge">rails new</code>. And a database. And an install process. And a ton of other things which have no use for the small task at hand. And, in the end, it just… whispers HTML into an SMTP server.</p>

<p>But we have React Email, don’t we? We do! Except that I don’t necessarily like React (and do not find it suitable for this use case at all), it still produces HTML, I need to learn an ecosystem I don’t want to learn that intimately - and, in the end, it just… whispers HTML into an SMTP server.</p>

<p>But we could use Apple Mail or Gmail right? Actually, no. We can’t because we do need some responsive CSS in the email, and we need to understand how big the fonts are going to be. We need to make sure the image occupies a certain amount of the width of the layout - and, in general, do some <em>minimal but essential</em> art-directing there. Apple Mail and GMail’s formatting tools are, after all, very limited WYSIWYG tools for the same - and you don’t get to control the HTML they output in a fine manner. Yes, there are <a href="https://mailtrap.io/blog/html-email-gmail/">hacky workarounds for bypassing the fact that GMail product teams think their users are stupid</a> – but why should we avail ourselves to this? And - it doesn’t even work all that well, honestly. Yes, Steve Jobs himself used to use this formatting with Mail.app back when it was running on his NeXT Cube - but did he really have good CSS features back then, and did he have a smartphone?</p>

<p>Sorry, I just want to write the HTML myself. I am more than capable.</p>

<h2 id="but-what-then">But what, then?</h2>

<p>In the spirit of the UNIX philosophy - a tool that can accept HTML and its assets on one end, and output an “emailish thing” (or deliver a message via SMTP) on the other end. It would work like this:</p>

<ol>
  <li>A magical <code class="language-plaintext highlighter-rouge">musherator</code> script would be conjured</li>
  <li>I would create the <code class="language-plaintext highlighter-rouge">email.html</code> next to the <code class="language-plaintext highlighter-rouge">musherator</code> script, and edit it with an HTML preview of my editor</li>
  <li>I would then run <code class="language-plaintext highlighter-rouge">cat email.html &gt; musherator</code> and Mail.app would open me a rendered preview</li>
  <li>If I was satisfied with the look in Mail.app I would enable a flag and do <code class="language-plaintext highlighter-rouge">cat email.html &gt; musherator --deliver-preview=me@julik.nl</code></li>
</ol>

<p>And it turns out that we can assemble one from the bits and bobs usually helpful with Rails mail sending (and, with regards to <code class="language-plaintext highlighter-rouge">mail</code>, <em>helpful</em> is a gross understatement - ActionMailer uses it as foundation):</p>

<ul>
  <li><a href="https://rubygems.org/gems/premailer">Premailer</a></li>
  <li><a href="https://rubygems.org/gems/nokogiri">Nokogiri</a></li>
  <li><a href="https://rubygems.org/gems/mail">Mail</a></li>
</ul>

<p>These three will likely already be part of your Rails app, you might just never have had the need to use them directly. Let’s get down to business.</p>

<p>First, we’ll need to have a script. For these one-off scripts I really prefer <a href="https://bundler.io/guides/bundler_in_a_single_file_ruby_script.html">inline Bundler</a> because it is compact and tight:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env ruby</span>

<span class="nb">require</span> <span class="s1">'bundler/inline'</span>

<span class="n">gemfile</span> <span class="k">do</span>
  <span class="n">source</span> <span class="s1">'https://rubygems.org'</span>
  <span class="n">gem</span> <span class="s1">'premailer'</span>
  <span class="n">gem</span> <span class="s1">'nokogiri'</span>
  <span class="n">gem</span> <span class="s1">'base64'</span>
  <span class="n">gem</span> <span class="s1">'mail'</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now, we need to read our HTML email and parse it. We need to do this “MIME email conversion thing”, though.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">premailer</span> <span class="o">=</span> <span class="no">Premailer</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="vg">$stdin</span><span class="p">,</span> <span class="ss">warn_level: </span><span class="no">Premailer</span><span class="o">::</span><span class="no">Warnings</span><span class="o">::</span><span class="no">SAFE</span><span class="p">)</span>
<span class="n">mail</span> <span class="o">=</span> <span class="no">Mail</span><span class="p">.</span><span class="nf">new</span>
<span class="n">mail</span><span class="p">.</span><span class="nf">part</span> <span class="ss">:content_type</span> <span class="o">=&gt;</span> <span class="s2">"multipart/mixed"</span> <span class="k">do</span> <span class="o">|</span><span class="n">p1</span><span class="o">|</span>
  <span class="n">p1</span><span class="p">.</span><span class="nf">part</span> <span class="ss">:content_type</span> <span class="o">=&gt;</span> <span class="s2">"multipart/related"</span> <span class="k">do</span> <span class="o">|</span><span class="n">p2</span><span class="o">|</span>
    <span class="n">html_with_cids</span> <span class="o">=</span> <span class="n">ingest_and_rewrite_images</span><span class="p">(</span><span class="n">premailer</span><span class="p">.</span><span class="nf">to_inline_css</span><span class="p">,</span> <span class="n">p2</span><span class="p">)</span>
    <span class="n">p2</span><span class="p">.</span><span class="nf">part</span> <span class="ss">:content_type</span> <span class="o">=&gt;</span> <span class="s2">"multipart/alternative"</span><span class="p">,</span> <span class="ss">:content_disposition</span> <span class="o">=&gt;</span> <span class="s2">"inline"</span> <span class="k">do</span> <span class="o">|</span><span class="n">p3</span><span class="o">|</span>
      <span class="n">p3</span><span class="p">.</span><span class="nf">part</span> <span class="ss">:content_type</span> <span class="o">=&gt;</span> <span class="s2">"text/plain; charset=utf-8"</span><span class="p">,</span> <span class="ss">:body</span> <span class="o">=&gt;</span> <span class="n">premailer</span><span class="p">.</span><span class="nf">to_plain_text</span>
      <span class="n">p3</span><span class="p">.</span><span class="nf">part</span> <span class="ss">:content_type</span> <span class="o">=&gt;</span> <span class="s2">"text/html; charset=utf-8"</span><span class="p">,</span> <span class="ss">:body</span> <span class="o">=&gt;</span> <span class="n">html_with_cids</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
<span class="n">mail</span><span class="p">.</span><span class="nf">from</span> <span class="s2">"Julik &lt;the-person@gmail.com&gt;"</span>
<span class="n">mail</span><span class="p">.</span><span class="nf">to</span> <span class="s2">"test@example.com"</span>
<span class="n">mail</span><span class="p">.</span><span class="nf">subject</span> <span class="s2">"Musherator HTML test </span><span class="si">#{</span><span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">.</span><span class="nf">strftime</span><span class="p">(</span><span class="s2">"%Y-%m-%d %H:%M"</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span>

<span class="n">test_email_filename</span> <span class="o">=</span> <span class="s2">"test.eml"</span>
<span class="no">File</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="n">test_email_filename</span><span class="p">,</span> <span class="s2">"wb"</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">fo</span><span class="o">|</span> <span class="n">fo</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">mail</span><span class="p">.</span><span class="nf">to_s</span><span class="p">)</span> <span class="p">}</span>
<span class="sb">`open </span><span class="si">#{</span><span class="n">test_email_filename</span><span class="si">}</span><span class="sb">`</span> <span class="c1"># Mail.app opens .emls</span>
</code></pre></div></div>

<p>Our email is going to be a <em>multipart message.</em> For example, the HTML rendition of your email body is a <em>part.</em> The plaintext rendition is also a <em>part.</em> Images included in your email are also <em>parts</em> and every attachment too! It’s like a few Russian dolls of those parts, and parts can actually <em>reference</em> each other. Note that we specify <code class="language-plaintext highlighter-rouge">multipart/related</code> for the same representations of the same resource, and attach our images under our <code class="language-plaintext highlighter-rouge">p2</code> part.</p>

<p>Normally, if you include an image in an email message, it gets its own MIME part - and gets also rendered by the email client as an attachment. However (!) - if that part is a child of a <code class="language-plaintext highlighter-rouge">multipart/related</code> part - most email clients won’t show it as an attachment, as those clients are smart enough to know that it is likely used in rendering the HTML part, and for nothing else. This is what the <code class="language-plaintext highlighter-rouge">ingest_and_rewrite_images</code> is for:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">ingest_and_rewrite_images</span><span class="p">(</span><span class="n">html_string</span><span class="p">,</span> <span class="n">into_mail_mime_part</span><span class="p">)</span>
  <span class="c1"># Add an attachment as a multipart part, under the /related multipart part.</span>
  <span class="c1"># Since it is related, these attachments will not show as downloadable attachments</span>
  <span class="c1"># - but they will be usable within the same part!</span>
  <span class="c1"># Once an image is attached to the part, we can get its URL (which uses a generated cid:)</span>
  <span class="c1"># and replace the src in the HTML with that.</span>
  <span class="n">noko_doc</span> <span class="o">=</span> <span class="no">Nokogiri</span><span class="o">::</span><span class="no">HTML</span><span class="p">(</span><span class="n">html_string</span><span class="p">)</span>
  <span class="n">noko_doc</span><span class="p">.</span><span class="nf">css</span><span class="p">(</span><span class="s2">"img"</span><span class="p">).</span><span class="nf">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">image_node</span><span class="o">|</span>
    <span class="n">image_relative_path</span> <span class="o">=</span> <span class="n">image_node</span><span class="p">[</span><span class="s2">"src"</span><span class="p">]</span>
    <span class="k">raise</span> <span class="s2">"Referenced image </span><span class="si">#{</span><span class="n">image_relative_path</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2"> not found"</span> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">exist?</span><span class="p">(</span><span class="n">image_relative_path</span><span class="p">)</span>

    <span class="n">fn</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">basename</span><span class="p">(</span><span class="n">image_relative_path</span><span class="p">)</span>
    <span class="n">into_mail_mime_part</span><span class="p">.</span><span class="nf">attachments</span><span class="p">[</span><span class="n">fn</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="ss">filename: </span><span class="n">image_relative_path</span><span class="p">}</span>
    <span class="n">cid_url</span> <span class="o">=</span> <span class="n">into_mail_mime_part</span><span class="p">.</span><span class="nf">attachments</span><span class="p">[</span><span class="n">fn</span><span class="p">].</span><span class="nf">url</span>
    <span class="n">image_node</span><span class="p">[</span><span class="s2">"src"</span><span class="p">]</span> <span class="o">=</span> <span class="n">cid_url</span>
  <span class="k">end</span>
  <span class="n">noko_doc</span><span class="p">.</span><span class="nf">to_html</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">cid_url</code> is a string containing a special URL using the <code class="language-plaintext highlighter-rouge">cid://</code> scheme - <code class="language-plaintext highlighter-rouge">cid</code> stands for “Content-ID” and you can read all about it in this <a href="https://www.rfc-editor.org/rfc/rfc2392">wonderful RFC.</a> When you add a file to a multipart part it gets a CID, which you then can reference in your HTML layout. Should an image be used multiple times, it will be reused because we store our images as a <code class="language-plaintext highlighter-rouge">Hash</code>. The <code class="language-plaintext highlighter-rouge">mail</code> gem takes care of actually reading the file for you.</p>

<p>And once we are done with that, we can actually - being on a Mac - write out our message as an <a href="https://www.wikihow.com/Open-EML-Files">.eml file</a> - as this is what the Mail gem outputs via its <code class="language-plaintext highlighter-rouge">Mail#write</code> method, and shell out to <code class="language-plaintext highlighter-rouge">open</code>. Mail.app on macOS natively supports <code class="language-plaintext highlighter-rouge">.eml</code> files - if you drag your emails out of it into a folder in the Finder, the operation also produces <code class="language-plaintext highlighter-rouge">.eml</code> files, one per email message!</p>

<h2 id="actually-whispering-via-smtp">Actually whispering via SMTP</h2>

<p>Easy. To see how the email looks in other clients, we need to… send it:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Mail</span><span class="p">.</span><span class="nf">defaults</span> <span class="k">do</span>
  <span class="n">delivery_method</span> <span class="ss">:smtp</span><span class="p">,</span>
    <span class="ss">address: </span><span class="s2">"smtp.gmail.com"</span><span class="p">,</span>
    <span class="ss">port: </span><span class="mi">587</span><span class="p">,</span>
    <span class="ss">user_name: </span><span class="s2">"the-person@gmail.com"</span><span class="p">,</span>
    <span class="ss">authentication: </span><span class="s2">"plain"</span><span class="p">,</span>
    <span class="ss">password: </span><span class="s2">"right there"</span>
<span class="k">end</span>
<span class="n">mail</span><span class="p">.</span><span class="nf">deliver!</span>
</code></pre></div></div>

<p>Note that for this to work you need to enable Gmail <a href="https://support.google.com/accounts/answer/185833?hl=en">app passwords</a> to have direct access to SMTP. But if you are using an email provider that is not Gmail - you likely can configure it here as well.</p>

<p>Needless to say, your script can also do the actual delivery of the transactional email! If you do it that way, make sure to:</p>

<ul>
  <li>Annotate in the body why the recipient is receiving it</li>
  <li>Include quick and clear instructions on how to be removed from your mailing</li>
  <li>…and any other things legally required for such emails in your locality</li>
  <li>Use the <code class="language-plaintext highlighter-rouge">Bcc:</code> for addresses.</li>
</ul>

<p>Nobody likes spam. Do not send spam.</p>

<p>If you can write the darn HTML and you know what you are doing - use something like this and get rapid experimentation and a near-instant feedback loop. This kind of developer-friendly workflow is really important, and it’s unfortunate that most commercial email platforms don’t offer this kind of experience at a reasonable price for one-off users.</p>

<p>You can find the <a href="https://gist.github.com/julik/a79665c2d441b426f01162f1f1c6ffd8">gist with the Musherator here.</a></p>]]></content><author><name></name></author><summary type="html"><![CDATA[A few months ago my partner-in-love-and-in-crime came with a seemingly innocuous request, which went as follows: We have an event coming up, and I need to send out a press release via email. It’s simple enough - just a couple of images and a few blurbs of text. I can’t seem to be able to make it look good in Gmail nor in Apple Mail. How does one do such a thing? Now, we computer-savvy household members know darn well that HTML email is, on the list of terrible IT things we have to help others with, right below the “can we get this printer to work?”. It can get… challenging. And yet, given that I have done this “HTML email” thing for a while – this piqued my curiosity. How hard can it be, thought I, to manually code an HTML email with images - and then use some Advanced Technology™ to turn it into a proper HTML email? While doing that using current commercial platforms turned out to be very hard indeed - for reasons having nothing to do with technology – I did come to an elegant solution that is usable locally. Exactly what I wanted: Layout an HTML email by source editing Turn it into an actual email (there is a file format for that!) Preview it in Apple Mail (on macOS and an iPhone) and in Gmail (desktop Web and iOS) …rinse and repeat until it looks great The real “aha moment” came when I realized that the tools I needed were already sitting right there in my Rails stack, just waiting to be used in a slightly different way. Premailer, Nokogiri, and the Mail gem - these aren’t exotic dependencies or bleeding-edge libraries. They’re battle-tested, well-documented tools. Curious where I ended up with that? Read on!]]></summary></entry><entry><title type="html">Data over time</title><link href="https://blog.julik.nl/2025/07/data-over-time" rel="alternate" type="text/html" title="Data over time" /><published>2025-07-09T00:00:00+00:00</published><updated>2025-07-09T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/07/data-over-time</id><content type="html" xml:base="https://blog.julik.nl/2025/07/data-over-time"><![CDATA[<p>Since 2021 I have been working at <a href="https://cheddar.me">Cheddar Payments</a>, which is a fledgling fintech startup in the UK. It was a substantial change from <a href="https://github.com/WeTransfer">WeTransfer</a> in terms of the problem domain, but also scale.</p>

<p>The scale at a B2C fintech is smaller, but the challenges are, in ways, much harder. And the biggest challenge - engineering-wise - is “data over time”. I’ve learned more about data over time than I would like, and it can be useful to share my experience.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="the-rails-way-is-harmful-for-data-over-time">The Rails Way™ is harmful for data over time</h2>

<p>Rails assumes building systems happen with a “current state of the world” database, filled with rows that get mutated. For example, this would be a very standard pattern in Rails:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">CustomerAccount</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="c1"># ...</span>
<span class="k">end</span>
<span class="n">account</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">balance: </span><span class="n">account</span><span class="p">.</span><span class="nf">balance</span> <span class="o">-</span> <span class="n">purchase_amount</span><span class="p">)</span>
</code></pre></div></div>

<p>While obvious and very quick to execute, this has a multitude of tradeoffs which will bite you down the road (and fairly quickly - in a fintech situation):</p>

<ul>
  <li>You don’t have visibility of specific account balance changes - only the aggregate</li>
  <li>You don’t have the ability to compute the balance at arbitrary points in time (what the balance “used to be”)</li>
  <li>You can’t easily make balance adjustments that get linked to the change they correct</li>
</ul>

<p>If you want to still maintain the Rails Way to some degree, start with a ledger-like accounting system using mutations:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">CustomerAccount</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">has_many</span> <span class="ss">:mutations</span><span class="p">,</span> <span class="o">-&gt;</span> <span class="p">{</span> <span class="n">order</span><span class="p">(</span><span class="ss">created_at: :desc</span><span class="p">)</span> <span class="p">}</span>

  <span class="k">def</span> <span class="nf">balance</span><span class="p">(</span><span class="ss">at: </span><span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">)</span>
    <span class="n">mutations</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="s2">"created_at &lt; ?"</span><span class="p">,</span> <span class="n">at</span><span class="p">).</span><span class="nf">sum</span><span class="p">(</span><span class="ss">:amount</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">Mutation</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">belongs_to</span> <span class="ss">:customer_account</span>
<span class="k">end</span>

<span class="n">account</span><span class="p">.</span><span class="nf">mutations</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">amount: </span><span class="o">-</span><span class="n">purchase_amount</span><span class="p">)</span> <span class="c1"># Negative amounts indicate debits</span>
</code></pre></div></div>

<p>This gives you a large set of conveniences - you can audit specific mutations, monitor the balance of the account at a specific point in time, make predictions and accurate corrections.</p>

<p>Caveat: the balance calculation above assumes a single-threaded environment. In reality, balance calculations may be taking place at the same time as INSERT operations, and the result could depend on your database’s transaction isolation level, so if you want no mutations to get added when querying balance - locking will be on you.</p>

<p>There are double-entry ledger systems for Ruby and for Rails but we found most of them do “too much of what we don’t need and not enough of what we do need”.</p>

<p>This applies to many models, not only some sort of <code class="language-plaintext highlighter-rouge">Account</code>. If you have, for example, a cashback someone has received - or a running mortgage - and these get adjustments over time, it is much (and I mean: orders of magnitude) better if you do this via “mutations” that concatenate, and not via changes to attributes.</p>

<h2 id="attribute-change-logs-only-get-you-so-far">Attribute change logs only get you so far</h2>

<p>Yes, you can record changes to the attributes of a particular record using something like <a href="https://github.com/paper-trail-gem/paper_trail">paper_trail</a> or <a href="https://github.com/palkan/logidze">logidze</a> - but they all have a shortcoming that you would want to consider. While debugging things does get easier because you know “the key 3 elements” - <em>what changed, from and to what, and when</em> - you do not have a clear description of <em>why</em> it changed.</p>

<p>Yes, the balance was decremented from 10 to 8. Was it because there was a mistake in the date and time calculation? Was it because some rate changed during that time? Was it because the person received a discretionary payout of some kind which got returned?</p>

<p>So what I believe is a better approach - and every time we used it at Cheddar, it turned out to be the right choice - is to apply the “mutation records” and aggregate the “state at T” from them. It is, in fact, a limited form of event sourcing.</p>

<p>If you want to discover more about event sourcing, <a href="https://ismaelcelis.com/">Ismael Celis</a> has a growing corpus of articles on the topic.</p>

<h2 id="intervals-are-tricky">Intervals are tricky</h2>

<p>Another pervasive concept in “data-over-time” systems is an <em>interval.</em> Intervals are pretty much all “start-inclusive and end-exclusive”, and are most frequently between times - but sometimes also between dates. A good example of an “interval-like” item in other systems would be a calendar entry.</p>

<p>Usually you would have something like</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ScopeGrantInterval</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="c1"># datetime :starts_at</span>
  <span class="c1"># datetime :ends_before</span>

  <span class="n">scope</span> <span class="ss">:covering</span><span class="p">,</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">t</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">where</span><span class="p">(</span><span class="s2">"starts_at &lt;= :t AND (ends_before &gt; :t OR ends_before IS NULL)"</span><span class="p">,</span> <span class="p">{</span><span class="n">t</span><span class="p">:})</span>
  <span class="p">}</span>

  <span class="k">def</span> <span class="nf">create_next!</span>
    <span class="n">t</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span>
    <span class="n">update!</span><span class="p">(</span><span class="ss">ends_before: </span><span class="n">t</span><span class="p">)</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">starts_at: </span><span class="n">t</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">covers?</span><span class="p">(</span><span class="n">time</span><span class="p">)</span>
    <span class="n">time</span> <span class="o">&gt;=</span> <span class="n">starts_at</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="n">ends_before</span><span class="p">.</span><span class="nf">nil?</span> <span class="o">||</span> <span class="n">time</span> <span class="o">&lt;</span> <span class="n">ends_before</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Intervals are tricky because they can utterly break down unless you hold them “just right”:</p>

<ul>
  <li>You want to place mnemonic hints everywhere you can whether a timestamp indicates an inclusive or exclusive containment. We settled on suffixing columns which are supposed to be queried non-inclusively with <code class="language-plaintext highlighter-rouge">_before</code> and <code class="language-plaintext highlighter-rouge">_after</code> instead of <code class="language-plaintext highlighter-rouge">_at</code> - this helps a ton.</li>
  <li>Intervals can be tricky to query - you want <code class="language-plaintext highlighter-rouge">WHERE starts_at &gt;= ? AND ends_before &lt; ?</code>, not <code class="language-plaintext highlighter-rouge">WHERE ? BETWEEN starts_at AND ends_before</code></li>
  <li>Use a sophisticated-enough DB that has <a href="https://stackoverflow.com/a/62392163/153886">range-type indexes</a> and can create related constraints</li>
  <li>Intervals can - and will - have gaps, and your system should be able to deal with that. Both business-logic-wise and in terms of performance.</li>
</ul>

<p>Intervals are likely to emerge with exchange rates, mortgage rates, limited discounts or boosts - any temporal entities which start at a certain point in time, end at a certain point in time, or both.</p>

<h2 id="time-zones-are-also-intervals">Time zones are also intervals</h2>

<p>Time zones change over time. A timezone definition is not just a name and UTC offset - it’s also “in effect since” and “superseded at”.</p>

<p>If you want to convert “14:38 on 1st March 2009 in London” to UTC, you need the timezone rules that were in effect <em>at that time</em>, not the rules in effect now. The timezone database is a collection of intervals, not a static lookup table.</p>

<p>Treat it as queryable intervals and you’ll have a much better time (pun intended).</p>

<h2 id="timestamps-are-not-great-as-cache-keys">Timestamps are not great as cache keys</h2>

<p>With data over time, you pretty much always want <em>read-after-write</em> consistency. If you have performed a payment, you want all the resources you load after to reflect that payment having taken place - your list of payments, your account balance…</p>

<p>Rails, by default, uses the <code class="language-plaintext highlighter-rouge">updated_at</code> of a model as the cache key, combined with the model class name. This kind of works (example from <a href="https://logik-matchbook.org">logik-matchbook</a>):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">matches</span><span class="p">(</span><span class="n">dev</span><span class="p">)</span><span class="o">&gt;</span> <span class="no">MatchboxShader</span><span class="p">.</span><span class="nf">first!</span><span class="p">.</span><span class="nf">cache_key</span>
  <span class="no">MatchboxShader</span> <span class="no">Load</span> <span class="p">(</span><span class="mf">0.1</span><span class="n">ms</span><span class="p">)</span>  <span class="no">SELECT</span> <span class="s2">"matchbox_shaders"</span><span class="p">.</span><span class="nf">*</span> <span class="no">FROM</span> <span class="s2">"matchbox_shaders"</span> <span class="no">ORDER</span> <span class="no">BY</span> <span class="s2">"matchbox_shaders"</span><span class="o">.</span><span class="s2">"id"</span> <span class="no">ASC</span> <span class="no">LIMIT</span> <span class="p">?</span>  <span class="p">[[</span><span class="s2">"LIMIT"</span><span class="p">,</span> <span class="mi">1</span><span class="p">]]</span>
<span class="o">=&gt;</span> <span class="s2">"matchbox_shaders/2-20131224152413220104"</span>
</code></pre></div></div>

<p>but is not good enough. Imagine the following scenario:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Process 1</span>
<span class="n">wallet</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">balance: </span><span class="no">Money</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="s2">"USD"</span><span class="p">))</span>

<span class="c1"># Process 2</span>
<span class="n">wallet</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">balance: </span><span class="no">Money</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="s2">"USD"</span><span class="p">))</span>
</code></pre></div></div>

<p>Just incidentally, these two processes happen to perform their operation at exactly the same time. What is the <code class="language-plaintext highlighter-rouge">cache_key</code> that is going to be used for caching the model after, and what is the state of the database going to be? The cache key will change, but it is going to reflect the change applied by just one of the processes - depending on when the cached snapshot of the <code class="language-plaintext highlighter-rouge">Wallet</code> gets generated and stored. Observe:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Process 1</span>
<span class="n">wallet</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">balance: </span><span class="no">Money</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="s2">"USD"</span><span class="p">))</span>

<span class="c1"># Process 3</span>
<span class="n">wallet</span><span class="p">.</span><span class="nf">cache_key</span> <span class="c1">#=&gt; Returns the cache key based on the timestamp of the update from process 1</span>

<span class="c1"># Process 2</span>
<span class="n">wallet</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">balance: </span><span class="no">Money</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">500</span><span class="p">,</span> <span class="s2">"USD"</span><span class="p">))</span> <span class="c1"># Touches the wallet, but does not change the value of the timestamp</span>

<span class="c1"># Process 4</span>
<span class="n">wallet</span><span class="p">.</span><span class="nf">cache_key</span> <span class="c1">#=&gt; Returns the cache key based on the timestamp of the update from process 2, which is the same as from process 1. Wallet with $5 balance gets served.</span>
</code></pre></div></div>

<p>While <code class="language-plaintext highlighter-rouge">update!</code> does set the <code class="language-plaintext highlighter-rouge">updated_at</code> timestamp to the current time, the current time itself may be identical across both processes if they execute within the same second (or even millisecond, depending on your database’s timestamp precision). This is why relying solely on timestamps for cache keys can be problematic.</p>

<p>So, if you want meaningful <a href="https://sourcediving.com/how-we-improved-our-rails-apps-performance-with-conditional-get-requests-35a7a472a0b9">conditional get</a> or meaningful cache keys, you need one of the two possible additions:</p>

<ul>
  <li>Use <a href="https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic/ClassMethods.html">optimistic locking</a> and integrate the lock version into your cache key, or</li>
  <li>Use <a href="https://en.wikipedia.org/wiki/Content-addressable_storage">content-addressable hashes</a> which capture the entire state of the record instead of just the update timestamp</li>
</ul>

<p>For the former, the following works well enough:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">cache_key</span>
  <span class="n">without_version</span> <span class="o">=</span> <span class="k">super</span>
  <span class="k">return</span> <span class="n">without_version</span> <span class="k">if</span> <span class="n">new_record?</span>
  <span class="k">return</span> <span class="n">without_version</span> <span class="k">unless</span> <span class="n">locking_enabled?</span>
  <span class="p">[</span><span class="n">without_version</span><span class="p">,</span> <span class="n">public_send</span><span class="p">(</span><span class="n">locking_column</span><span class="p">)].</span><span class="nf">join</span><span class="p">(</span><span class="s2">"/"</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="a-set-is-not-a-list-and-vice-versa">A set is not a list, and vice-versa</h2>

<p>Webhooks are great, and <a href="https://martinfowler.com/eaaDev/EventSourcing.html">event sourcing</a> is great. There is a large caveat, however: if you go with an event sourcing system, pick one which has strict ordering that <em>you</em> control. Even with a sequence of <code class="language-plaintext highlighter-rouge">Mutation</code> records on a <code class="language-plaintext highlighter-rouge">CustomerAccount</code> from earlier:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">CustomerAccount</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">has_many</span> <span class="ss">:mutations</span><span class="p">,</span> <span class="o">-&gt;</span> <span class="p">{</span> <span class="n">order</span><span class="p">(</span><span class="ss">created_at: :desc</span><span class="p">)</span> <span class="p">}</span>

  <span class="k">def</span> <span class="nf">balance</span><span class="p">(</span><span class="ss">at: </span><span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">)</span>
    <span class="n">mutations</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="s2">"created_at &lt; ?"</span><span class="p">,</span> <span class="n">at</span><span class="p">).</span><span class="nf">sum</span><span class="p">(</span><span class="ss">:amount</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">Mutation</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">belongs_to</span> <span class="ss">:customer_account</span>
<span class="k">end</span>
</code></pre></div></div>

<p>we can’t reliably ascertain the state of the account <em>between</em> two or more <code class="language-plaintext highlighter-rouge">Mutation</code> events if they occur at the same time:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">results</span> <span class="o">=</span> <span class="n">account</span><span class="p">.</span><span class="nf">mutations</span><span class="p">.</span><span class="nf">pluck</span><span class="p">(</span><span class="ss">:amount</span><span class="p">,</span> <span class="ss">:created_at</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#  {amount: -120, created_at: "2024-05-12-00:00:01" },</span>
<span class="c1">#  {amount: +30, created_at: "2024-05-12-00:00:01" }</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>You can’t really say which one of these came first. In general, this is not a very big problem <em>if you pay attention to your cutoff</em>. But there are situations where events depend on each other <em>and</em> on the ordering. For example, if you need to do a balance check before adding 30 cents to the account, you probably want to do it after removing the 120 cents.</p>

<p>Different systems approach this differently. For example, in <a href="https://openbankinguk.github.io/read-write-api-site3/v4.0/resources-and-data-models/aisp/Transactions.html">OpenBanking</a> the transactions you can download do not specify exact ordering. But they do, usually, contain a <code class="language-plaintext highlighter-rouge">Balance</code> structure which specifies the balance before and after the transaction took place.</p>

<p>If we want to do strict ordering in our <code class="language-plaintext highlighter-rouge">mutations</code>, we can add a sequence number when inserting them. This is not as robust as using a trigger, but will provide ordering:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># ..._migration.rb</span>
<span class="n">add_column</span> <span class="ss">:mutations</span><span class="p">,</span> <span class="ss">:seq</span><span class="p">,</span> <span class="ss">:bigint</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">true</span>
<span class="n">add_index_</span> <span class="ss">:mutations</span><span class="p">,</span> <span class="p">[</span><span class="ss">:seq</span><span class="p">,</span> <span class="ss">:customer_account_id</span><span class="p">],</span> <span class="ss">unique: </span><span class="kp">true</span>

<span class="k">class</span> <span class="nc">Mutation</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">belongs_to</span> <span class="ss">:customer_account</span>
  <span class="n">after_create</span> <span class="k">do</span> <span class="o">|</span><span class="n">m</span><span class="o">|</span>
    <span class="n">m</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">connection</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="o">&lt;&lt;~</span><span class="no">SQL</span><span class="p">)</span><span class="sh">
      UPDATE </span><span class="si">#{</span><span class="n">m</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">table_name</span><span class="si">}</span><span class="sh"> SET seq = (
        SELECT COUNT(*) + 1 FROM </span><span class="si">#{</span><span class="n">m</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">table_name</span><span class="si">}</span><span class="sh">
        WHERE customer_account_id = </span><span class="si">#{</span><span class="n">m</span><span class="p">.</span><span class="nf">customer_account_id</span><span class="si">}</span><span class="sh">
      ) WHERE id = </span><span class="si">#{</span><span class="n">m</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="sh">
</span><span class="no">    SQL</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>You can use something more sophisticated like PostgreSQL sequences, but you probably only care about sequencing within a customer account anyway. The above solution is not ideal but will already give you more confident sequencing.</p>

<h1 id="webhooks-are-usually-a-set">Webhooks are usually a set</h1>

<p>Imagine we have these events for a hypothetical <code class="language-plaintext highlighter-rouge">Payment</code> with <code class="language-plaintext highlighter-rouge">ID=123</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> payment_id | created_at       | event_type
------------|------------------|------------
 123        | 2024-05-12 00:00 | charged
 123        | 2024-05-12 00:01 | fraud_reported
 123        | 2024-05-12 00:02 | chargeback
 123        | 2024-05-12 00:03 | refund
</code></pre></div></div>

<p>The peculiarity here is that <code class="language-plaintext highlighter-rouge">created_at</code> is <em>not</em> the timestamp of when the event was generated. These events come from an external system via webhooks, and the <code class="language-plaintext highlighter-rouge">created_at</code> is our - generated - timestamp. We can try and derive a sequence number “in order of reception” of those webhooks - where the <code class="language-plaintext highlighter-rouge">seq</code> value is guaranteed to be unique in scope of <code class="language-plaintext highlighter-rouge">payment_id</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> payment_id | seq | created_at       | event_type
------------|-----|------------------|------------
 123        | 1   | 2024-05-12 00:00 | charged
 123        | 2   | 2024-05-12 00:01 | fraud_reported
 123        | 3   | 2024-05-12 00:01 | chargeback
 123        | 4   | 2024-05-12 00:02 | refund
</code></pre></div></div>

<p>But the issue here is that this ordering has been divinated by <em>us</em> - and not set by the sender of the webhooks. In actuality, the provider may have generated these events in the following order:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> payment_id | event_type
------------|------------
 123        | charged
 123        | chargeback
 123        | refund
 123        | fraud_reported
</code></pre></div></div>

<p>but due to their sending HTTP client being slow, our receiving HTTP endpoint being slow, our database introducing a bit of a delay to serialize transactions - <em>their</em> order does not match <em>ours.</em> And their ordering may even not be defined. The problem is going to emanate from us trying to assume their events are ordered while they are not.</p>

<p>Here is a simple exercise I like to do: grab an array of all the events you may have. For example - <code class="language-plaintext highlighter-rouge">[:charged, :chargeback, :refund, :fraud_reported]</code>. Use the following call to see how many possible orderings you can have for them:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="ss">:charged</span><span class="p">,</span> <span class="ss">:chargeback</span><span class="p">,</span> <span class="ss">:refund</span><span class="p">,</span> <span class="ss">:fraud_reported</span><span class="p">].</span><span class="nf">permutation</span><span class="p">.</span><span class="nf">to_a</span><span class="p">.</span><span class="nf">length</span> <span class="c1"># =&gt; 24 possible orderings</span>
</code></pre></div></div>

<p>If you are finding yourself in a situation where you do not have a meaningful ordering of incoming events, here is what you can do in your tests:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="ss">:charged</span><span class="p">,</span> <span class="ss">:chargeback</span><span class="p">,</span> <span class="ss">:refund</span><span class="p">,</span> <span class="ss">:fraud_reported</span><span class="p">].</span><span class="nf">permutation</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">possible_ordering</span><span class="o">|</span>
  <span class="n">description</span> <span class="o">=</span> <span class="n">possible_ordering</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">" -&gt; "</span><span class="p">)</span>
  <span class="nb">test</span> <span class="s2">"with events in order of </span><span class="si">#{</span><span class="n">description</span><span class="si">}</span><span class="s2">"</span> <span class="k">do</span>
    <span class="n">assert</span> <span class="n">some_invariant1</span>
    <span class="n">assert</span> <span class="n">some_invariant2</span>
    <span class="c1"># ...any other tests that must be true for all orderings</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This does not handle the situations where events are missed (skipped), but at least it gives you some coverage. Anyway - unless your sender of webhooks/events specifies certain ordering invariants <em>or</em> gives you a sequence number/vector clock for the events they send, assume the events are <em>unordered.</em> Your event list is not a list anymore - it’s a set, where you need to exercise caution to establish some basic invariants. For example:</p>

<ul>
  <li>If the webhook sender gives you a timestamp, you can assume that the webhooks are ordered within certain <em>large</em> time buckets, or in clusters. So while you can’t have <em>exact</em> ordering, you can at least make guesses.</li>
  <li>If you are receiving, say, events from our previous example - you can assume that the <code class="language-plaintext highlighter-rouge">charged</code> event should always be present and it should always be considered first. To that end, you can buffer the other events, but not proceed to processing the events until you have recorded the <code class="language-plaintext highlighter-rouge">charged</code> event for that <code class="language-plaintext highlighter-rouge">payment_id</code>.</li>
</ul>

<p>And most providers - even Stripe - do not report event sequencing information in their webhooks. If you use webhooks between your systems, even having a sequence number for webhooks that you send which would be <code class="language-plaintext highlighter-rouge">SELECT COUNT(*) + 1 FROM generated_webhooks WHERE related_model_id = ?</code> will help establish causality on the receiving side.</p>

<p>Conversely, if you are only receiving and there is no sequencing - frequently the best course of action is to <em>not use the data from the webhook at all</em> - but to use the webhook as a signal to re-read the remote resource. Yes, it will be slower, but at the very least your local representation of the changed resource is going to be reasonably up-to-date.</p>

<p>Know when you are dealing with a set versus an ordered list and act accordingly. And do not let the transport deceive you. A Kafka topic or an EventEmitter will be delivering you messages serially and sequentially - sure, but do they get placed into that topic or server-sent-events stream in the order they got generated by the system? If there is no producer-defined ordering which is business-logic specific, the fact that the events arrive via a serial channel does not matter.</p>

<h2 id="what-i-didnt-cover">What I didn’t cover</h2>

<p>This article focuses on correctness over performance. I didn’t cover transaction isolation levels - those get drilled in programming courses. Just check out YouTube for “transaction isolation tutorial”. Proper data-over-time architecture, however, gets left by the wayside. But yes, you’ll need both. And yes, the ledger approach may scale poorly without materialized views or snapshots. But first, get the data model right.</p>

<h2 id="if-you-are-hungry-for-more">If you are hungry for more</h2>

<p>The general domain for this type of architectures is called <a href="https://en.wikipedia.org/wiki/Bitemporal_modeling">bitemporal modeling.</a> A great article to dip your feet into it can be found <a href="https://www.evalapply.org/posts/poor-mans-time-oriented-data-system/index.html">here</a> - everything there is easily transposable into Ruby if Clojure is not your jam.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Since 2021 I have been working at Cheddar Payments, which is a fledgling fintech startup in the UK. It was a substantial change from WeTransfer in terms of the problem domain, but also scale. The scale at a B2C fintech is smaller, but the challenges are, in ways, much harder. And the biggest challenge - engineering-wise - is “data over time”. I’ve learned more about data over time than I would like, and it can be useful to share my experience.]]></summary></entry><entry><title type="html">GETting conditionally - the bare basics</title><link href="https://blog.julik.nl/2025/06/getting-conditionally" rel="alternate" type="text/html" title="GETting conditionally - the bare basics" /><published>2025-06-13T00:00:00+00:00</published><updated>2025-06-13T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/06/getting-conditionally</id><content type="html" xml:base="https://blog.julik.nl/2025/06/getting-conditionally"><![CDATA[<p>A while ago, a prominent Vercel employee (two, actually) posted to the tune of:</p>

<blockquote>
  <p>Developers don’t get CDNs</p>
</blockquote>

<p><a href="https://www.linkedin.com/posts/rauchg_why-cdns-dont-really-work-site-is-dynamic-activity-7242525828834607105-x4IG/">Exhibit A</a> etc.</p>

<p>It is often that random tweets somehow get me into a frenzy – somebody is wrong on the internet, yet again. But when I gave this a second thought, I figured that… this statement has more merit than I would have wanted it to have.</p>

<p>It has merit because we do not know the very basics of cache control that are necessary (and there are not that many)!</p>

<p>It does not have merit in the sense that force-prefetching all of your includes through Vercel’s magic RSC-combine will not, actually, solve all your problems. They are talking in solutions that they sell, and what they are not emphasizing is that the issue is with the “developer slaps ‘Cache-Control’” part. Moreover: as I will explain, a lot of juice can be squeezed out of you by CDN providers exactly because your cache control is <em>not in order</em> and they offer you tools that kind of “force” your performance back into a survivable state. With some improvement for your users, and to the detriment of your wallet. But first, let’s rewind and see what those CDNs actually do.</p>

<p>CDNs use something called “conditional GET requests”. Conditional GET requests mean: <code class="language-plaintext highlighter-rouge">Cache-Control</code>. And even I, in my hubris, haven’t been using it correctly. After reviewing how it worked on a few of my own sites, I have overhauled my uses – and built up a “minimum understanding” of it which has been, to say the least, useful.</p>

<p>So, there it is: the absolute bare minimum of <code class="language-plaintext highlighter-rouge">Cache-Control</code> knowledge you may need for a public, mostly-static (CMS-driven, let’s say) website. Strap in, this is going to be wild.</p>

<p>And be mindful of one thing: I do not work for Vercel, CloudFlare, AWS or Fastly. I just like fast websites and I think you deserve to have your website go fast as well.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="what-are-web-caches-anyway">What are web caches, anyway?</h2>

<p>Put simply: web caches ensure that once you have downloaded something, you get to keep it as long as it doesn’t change. Or, at the very least, you will be downloading it from someplace that is faster - and closer to you - than the website you are originally trying to access.</p>

<p>Your browser has a cache (this is your <code class="language-plaintext highlighter-rouge">private</code> cache, just yours). Then there can be “intermediate” caches, which we are going to call “caching proxies”. At the very end of that chain there is the actual website you are downloading from - the <code class="language-plaintext highlighter-rouge">origin</code>.</p>

<p>If you are running the <code class="language-plaintext highlighter-rouge">origin</code> website - it is your responsibility to use those caches to the max - and, if possible, running one of your own. As a matter of fact, we are going to do just that.</p>

<p>In the times of yore, when dinosaurs roamed the earth and shared web hosting was commonplace - caching proxies were actually much more pervasive than they are now. To begin with, many hosting providers would forcibly install something called <a href="https://www.squid-cache.org/">squid</a> in front of all websites hosted by them. No “ifs”, no “buts” - if you were hosting with a certain provider, they would stick a caching proxy in the middle regardless of whether you wanted it or not, whether it was beneficial for use case - or not.</p>

<p>Next, company intranets were a thing and internet access (especially: fast internet access) was expensive. Doubly so - for business users, and in offices. So - in addition to the caching proxies installed by hosting providers - you would also have the “internal” caching proxies. Remember the <a href="https://mcpmag.com/articles/2000/09/26/microsoft-offers-web-caching-with-isa-2000.aspx">Internet Security and Acceleration Server</a> from Microsoft? Well, the “Acceleration” part was about doing caching proxying for your entire office.</p>

<p>And then, with pervasive broadband - and both the more widespread deployment of SSL and the emergence of virtual machine based hosting - those caching proxies kind of faded out of the picture, except for the use cases of the biggest, most frequented web resources on the planet. The Akamai CDN is old, and Apple has been using Akamai since… 1999? What is Akamai, you may ask? Well… it is a caching proxy!</p>

<h2 id="a-cdn-versus-a-caching-proxy">A CDN versus a caching proxy</h2>

<p>For ease of understanding I defined it like this: a CDN is a sophisticated caching proxy with multiple caching nodes. An ideal CDN would have multiple tiers of caches, and have those caches geographically distributed to provide multiple POEs (“points of entry”) for users in different locations, with the “hottest” (most frequently accessed) pages being available in the caches closest to the user.</p>

<p>An ultimate “distribution strategy” would be something similar to where Netflix and YouTube have gone. They install “caching appliances” (caching web proxy servers, essentially) <a href="https://openconnect.netflix.com/en/">in hardware form,</a> right at the ISPs data center - so that when you go to watch the latest episode of <a href="https://www.whats-on-netflix.com/what-to-watch/best-new-netflix-series-in-2025/">Adolescence</a> you get it quickly and without your ISP having to go fetch it from Netflix - as your neighbor has already watched it yesterday, and it is in the ISP’s caching appliance Netflix has given them.</p>

<p>So, for a web developer like myself and you: <strong>a CDN is a caching proxy,</strong> just a fairly sophisticated one. The more sophisticated - the more “tricks” you can employ with it. For example, edge functions are basically scripts/small web apps which run on the CDN edge nodes, and can route, restrict or splice requests before they get to your web application.</p>

<p>But for 80% of use cases - or more - you can get by without any of that. No fancy Vercel prefetch, no fancy DOs, no edge functions - just the good, old <code class="language-plaintext highlighter-rouge">Cache-Control</code>. But it is a fickle mistress and you have to hold it right.</p>

<h2 id="for-static-files-cache-control-is-usually-good-already">For static files, cache control is usually good already</h2>

<p>How is it that we often don’t need to think about <code class="language-plaintext highlighter-rouge">Cache-Control</code> at all? Well, all of the modern webservers - no exception - are quite adept at setting cache-friendly headers for static files. It is not difficult to do, and since they do it so well - for most users this is actually fully transparent - including you.</p>

<p>But once the output comes from your web application - a good webserver will step aside and not touch your <code class="language-plaintext highlighter-rouge">Cache-Control</code> at all. If it does - either the server is misconfigured by mistake, or it is deliberately misconfigured to force caching on you - by a nefarious systems administrator at your hosting provider (or a semi-hostile devops team) to save costs. This is an interesting topic which we will revisit.</p>

<h2 id="learning-with-your-own-toys">Learning with your own toys</h2>

<p>I firmly believe that you can place little trust in things you can’t run locally. At the very minimum <em>a faithful reproduction</em> is also good enough, provided it works well. For Ruby web apps, there happens to be an old-but-good caching proxy solution which actually embeds inside your Rails (or Rack) application. It will intercept incoming requests, examine the responses you return to the client - and cache them as headers dictate. So, let’s put together a bare-bones Rack application which serves us a cheeky little piece of content:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">run</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="p">{</span>
  <span class="nb">require</span> <span class="s2">"securerandom"</span>
  <span class="n">request_id</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">uuid</span>
  <span class="n">headers</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"content-type"</span> <span class="o">=&gt;</span> <span class="s2">"text/plain"</span><span class="p">}</span>
  <span class="p">[</span><span class="mi">200</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="p">[</span><span class="s2">"Hello request </span><span class="si">#{</span><span class="n">request_id</span><span class="si">}</span><span class="s2">"</span><span class="p">]]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We will also need another piece of kit which I have been using for ages, and it is a <code class="language-plaintext highlighter-rouge">curl</code> shell alias. A shell alias is just simpler to set up, you can use an HTTP client call if you prefer that. The alias goes like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">alias </span><span class="nv">headercheck</span><span class="o">=</span><span class="s1">'curl -s -D - $1 -o /dev/null'</span>
</code></pre></div></div>

<p>and you run it like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>headercheck https://blog.julik.nl
HTTP/2 200 
server: GitHub.com
content-type: text/html<span class="p">;</span> <span class="nv">charset</span><span class="o">=</span>utf-8
last-modified: Fri, 23 May 2025 17:12:38 GMT
access-control-allow-origin: <span class="k">*</span>
etag: <span class="s2">"6830ac86-e960"</span>
expires: Thu, 12 Jun 2025 23:58:18 GMT
cache-control: max-age<span class="o">=</span>600
x-proxy-cache: MISS
x-github-request-id: 9BA7:3471C9:54FA19:55EEF4:684B6741
accept-ranges: bytes
age: 0
<span class="nb">date</span>: Thu, 12 Jun 2025 23:48:18 GMT
via: 1.1 varnish
x-served-by: cache-ams21075-AMS
x-cache: MISS
x-cache-hits: 0
x-timer: S1749772098.153089,VS0,VE132
vary: Accept-Encoding
x-fastly-request-id: 97b8fdb7083c48fc06e988ebcc32efd7281c0be0
content-length: 59744
</code></pre></div></div>

<p>It does something very simple: makes a GET request (not a HEAD, and that is important!) using <code class="language-plaintext highlighter-rouge">curl</code>, and then prints you just the headers. It discards the response body.</p>

<p>From this example, we can actually already see a few interesting things:</p>

<ul>
  <li>The presence of <code class="language-plaintext highlighter-rouge">etag</code> indicates that Github calculates some kind of checksum for the index page</li>
  <li>The <code class="language-plaintext highlighter-rouge">last-modified</code> defines the last modification date of the file</li>
  <li>The <code class="language-plaintext highlighter-rouge">via: 1.1 varnish</code> indicates that GH Pages is using a caching proxy of its own - one called <a href="https://varnish-cache.org/intro/">Varnish</a></li>
  <li>The <code class="language-plaintext highlighter-rouge">cache-control</code> says <code class="language-plaintext highlighter-rouge">max-age=600</code> - we will get to that</li>
  <li>The <code class="language-plaintext highlighter-rouge">vary: Accept-Encoding</code> says that any caching that applies for <em>us</em> is content-encoding specific (so a separate cache will be used for Gzip-compressed responses and for plain responses)</li>
  <li>The <code class="language-plaintext highlighter-rouge">etag</code> is a “strong” ETag - it is just a string in quotes, without the <code class="language-plaintext highlighter-rouge">\W</code> at the end. This means that the representation we have fetched is specific to the content-encoding (or, rather - the combo of our request headers mentioned in the <code class="language-plaintext highlighter-rouge">vary:</code> header)</li>
</ul>

<p>We are going to use this <code class="language-plaintext highlighter-rouge">headercheck</code> command to experiment with our local nano-CDN (which conveniently lives inside our Rack application).</p>

<p>Let’s complete our Rack web app with dependency definitions and “headercheck” it:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'bundler/inline'</span>

<span class="n">gemfile</span> <span class="k">do</span>
  <span class="n">source</span> <span class="s1">'https://rubygems.org'</span>
  <span class="n">gem</span> <span class="s1">'puma'</span><span class="p">,</span> <span class="s1">'~&gt; 6.4'</span><span class="p">,</span> <span class="ss">require: </span><span class="kp">false</span>
  <span class="n">gem</span> <span class="s1">'rack'</span>
<span class="k">end</span>

<span class="nb">require</span> <span class="s1">'puma'</span>
<span class="nb">require</span> <span class="s1">'rack'</span>

<span class="c1"># Define a simple Rack application</span>
<span class="n">app</span> <span class="o">=</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Builder</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
  <span class="n">run</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="p">{</span>
    <span class="nb">require</span> <span class="s2">"securerandom"</span>
    <span class="n">request_id</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">uuid</span>
    <span class="n">headers</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"content-type"</span> <span class="o">=&gt;</span> <span class="s2">"text/plain"</span><span class="p">}</span>
    <span class="p">[</span><span class="mi">200</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="p">[</span><span class="s2">"Hello request </span><span class="si">#{</span><span class="n">request_id</span><span class="si">}</span><span class="s2">"</span><span class="p">]]</span>
  <span class="p">}</span>
<span class="k">end</span>

<span class="c1"># Configure and start Puma server</span>
<span class="n">server</span> <span class="o">=</span> <span class="no">Puma</span><span class="o">::</span><span class="no">Server</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>
<span class="n">server</span><span class="p">.</span><span class="nf">add_tcp_listener</span> <span class="s1">'0.0.0.0'</span><span class="p">,</span> <span class="mi">3000</span>
<span class="no">STDERR</span><span class="p">.</span><span class="nf">puts</span> <span class="s2">"Puma server listening on port 3000"</span>
<span class="n">server</span><span class="p">.</span><span class="nf">run</span><span class="p">.</span><span class="nf">join</span>
</code></pre></div></div>

<p>When we <code class="language-plaintext highlighter-rouge">headercheck</code> it, we get:</p>

<pre><code class="language-`">$ headercheck http://localhost:3000
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 16
</code></pre>

<p>Since we are interested in the headers here - and headers get cached as well - let’s add our “sentinel value” (the request ID) to the headers. If it does not change between our invocations of <code class="language-plaintext highlighter-rouge">headercheck</code>, we will know that the cached output is being served:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"securerandom"</span>

<span class="c1"># Define a simple Rack application</span>
<span class="n">app</span> <span class="o">=</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Builder</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
  <span class="n">run</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">request_id</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">uuid</span>
    <span class="n">headers</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"x-request-id"</span> <span class="o">=&gt;</span> <span class="n">request_id</span><span class="p">,</span> <span class="s2">"content-type"</span> <span class="o">=&gt;</span> <span class="s2">"text/plain"</span><span class="p">}</span>
    <span class="p">[</span><span class="mi">200</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="p">[</span><span class="s2">"Hello request </span><span class="si">#{</span><span class="n">request_id</span><span class="si">}</span><span class="s2">"</span><span class="p">]]</span>
  <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>…and hit it with a couple of requests:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ headercheck http://localhost:3000
HTTP/1.1 200 OK
x-request-id: 3354a2e3-7ce1-49e7-b44c-5956c539017c
content-type: text/plain
Content-Length: 50

$ headercheck http://localhost:3000
HTTP/1.1 200 OK
x-request-id: 62ae58dd-7847-484d-8cee-57e150c90dd9
content-type: text/plain
Content-Length: 50
</code></pre></div></div>

<p>As designed, the <code class="language-plaintext highlighter-rouge">x-request-id</code> is different on every request - we have a completely dynamic web application. By default, it will not cache anything.</p>

<p>Now, let’s add <a href="https://github.com/rtomayko/rack-cache">rack-cache</a></p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">app</span> <span class="o">=</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Builder</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
  <span class="n">use</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Cache</span><span class="p">,</span>
    <span class="ss">metastore: </span><span class="s2">"file:rack-cache"</span><span class="p">,</span>
    <span class="ss">entitystore: </span><span class="s2">"file:rack-cache"</span><span class="p">,</span>
    <span class="ss">allow_reload: </span><span class="kp">true</span>

  <span class="n">run</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">request_id</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">uuid</span>
    <span class="n">headers</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"x-request-id"</span> <span class="o">=&gt;</span> <span class="n">request_id</span><span class="p">,</span> <span class="s2">"content-type"</span> <span class="o">=&gt;</span> <span class="s2">"text/plain"</span><span class="p">}</span>
    <span class="p">[</span><span class="mi">200</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="p">[</span><span class="s2">"Hello request </span><span class="si">#{</span><span class="n">request_id</span><span class="si">}</span><span class="s2">"</span><span class="p">]]</span>
  <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Initially, nothing changes:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ headercheck http://localhost:3000
HTTP/1.1 200 OK
x-request-id: dd4b4a1d-46c8-46ca-8a5c-a4d182424152
content-type: text/plain
date: Fri, 13 Jun 2025 00:07:32 GMT
x-rack-cache: miss
Content-Length: 50
</code></pre></div></div>

<p>We do see, however, that rack-cache has recorded a <em>cache miss</em> - it tried to satisfy the request using cached data, but it could not - so it has let the request “fall through” to our app.</p>

<p>Now let’s do some cache controlling:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">headers</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"x-request-id"</span> <span class="o">=&gt;</span> <span class="n">request_id</span><span class="p">,</span> <span class="s2">"content-type"</span> <span class="o">=&gt;</span> <span class="s2">"text/plain"</span><span class="p">}</span>
<span class="n">headers</span><span class="p">[</span><span class="s2">"cache-control"</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"max-age=600"</span>
</code></pre></div></div>

<p>and check the headers:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ headercheck http://localhost:3000
HTTP/1.1 200 OK
x-request-id: 49048426-fe8b-4a31-9828-0ef5fcfa831e
content-type: text/plain
cache-control: max-age=600
date: Fri, 13 Jun 2025 00:10:18 GMT
x-content-digest: 80c4a9709694ccde545395317456031aa50e08be
age: 0
x-rack-cache: miss, store
Content-Length: 50

$ headercheck http://localhost:3000
HTTP/1.1 200 OK
x-request-id: 49048426-fe8b-4a31-9828-0ef5fcfa831e
content-type: text/plain
cache-control: max-age=600
date: Fri, 13 Jun 2025 00:10:18 GMT
x-content-digest: 80c4a9709694ccde545395317456031aa50e08be
age: 1
x-rack-cache: fresh
Content-Length: 50
</code></pre></div></div>

<p>Now, rack-cache starts caching our app’s output. We limit the age of the cache, and we can see that rack-cache has also computed the <code class="language-plaintext highlighter-rouge">x-content-digest</code> of our response, and that it stays the same. We can also see that the value of <code class="language-plaintext highlighter-rouge">x-request-id</code> header does not change. Our little “CDN in a box” is doing its job, and you now have what is an equivalent of the Rails page cache.</p>

<p>It is also broken. Our request ID is generated at random on every page render, but now we cache that generated random request ID… for 10 minutes. And more: for example, imagine we want to to change the output of our little app, and we change our code and do a new deployment (in this case “deployment” means restarting the local Ruby script):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="mi">200</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="p">[</span><span class="s2">"Gruezi for request </span><span class="si">#{</span><span class="n">request_id</span><span class="si">}</span><span class="s2">"</span><span class="p">]]</span>
</code></pre></div></div>

<p>We make a request, and…</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ headercheck http://localhost:3000
HTTP/1.1 200 OK
x-request-id: 49048426-fe8b-4a31-9828-0ef5fcfa831e
content-type: text/plain
cache-control: max-age=600
date: Fri, 13 Jun 2025 00:10:18 GMT
x-content-digest: 80c4a9709694ccde545395317456031aa50e08be
age: 223
x-rack-cache: fresh
Content-Length: 50
</code></pre></div></div>

<p>Oops. We are in exactly the same dreadful spot the webmasters of the early 2000s would find themselves with a provider forcibly imposing a caching proxy on them. Your page is now firmly planted in the rack-cache, and won’t be excised and re-requested from your app until <code class="language-plaintext highlighter-rouge">age</code> turns 600 seconds or more. Bah.</p>

<p>What can we do about it?</p>

<h2 id="cache-validity">Cache validity</h2>

<p>When you talk about caching, a fairly common term is “cache invalidation”. It’s a weirdly chosen name, because there is nothing “invalid” about it. Rather, “validation” with caches means literally this: <strong>asking whether this cache is still fresh.</strong> The validation can be done using some sort of validation handle. For example, it can be a timestamp. We can let our app tell the caching proxy that the last modification time on our resource is X, and it then works as a <em>validator.</em> It will work in two ways:</p>

<ul>
  <li>It will inform the caching proxy better as to what <code class="language-plaintext highlighter-rouge">max-age</code> refers to. A proxy may choose to revalidate with the origin (our app) even if <code class="language-plaintext highlighter-rouge">max-age</code> has not yet lapsed, or it may not. We’ll get there</li>
  <li>It allows the proxy to ask our app: “Have you modified this resource since X?” If our app responds with “Nope, still the same” (which is the 304 status) - the caching proxy can choose to extend its cached version storage for another <code class="language-plaintext highlighter-rouge">max-age</code>, or to just serve the cached version now</li>
</ul>

<p>Since we are inside of a script and we <em>know</em> when we modify it - it is a file, dammit - we can grab the <code class="language-plaintext highlighter-rouge">mtime</code> of our script as our <code class="language-plaintext highlighter-rouge">Last-Modified</code>. It does need to be formatted using <code class="language-plaintext highlighter-rouge">Time#httpdate</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">headers</span><span class="p">[</span><span class="s2">"cache-control"</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"max-age=600"</span>
<span class="n">headers</span><span class="p">[</span><span class="s2">"last-modified"</span><span class="p">]</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">mtime</span><span class="p">(</span><span class="kp">__FILE__</span><span class="p">).</span><span class="nf">httpdate</span>
<span class="p">[</span><span class="mi">200</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="p">[</span><span class="s2">"Gruezi for request </span><span class="si">#{</span><span class="n">request_id</span><span class="si">}</span><span class="s2">"</span><span class="p">]]</span>
</code></pre></div></div>

<p>Now, when we do a few GETs in sequence, the behavior of the caching proxy changes:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ headercheck http://localhost:3000
HTTP/1.1 200 OK
x-request-id: a86dfec7-4c5b-4715-b83f-8bc4460b01de
content-type: text/plain
cache-control: max-age=600
last-modified: Fri, 13 Jun 2025 00:22:08 GMT
date: Fri, 13 Jun 2025 00:22:15 GMT
x-content-digest: e44ad5b6f016fa72eb8502e3d697c183b1958fed
age: 0
x-rack-cache: stale, invalid, store
Content-Length: 55

$ headercheck http://localhost:3000
HTTP/1.1 200 OK
x-request-id: a86dfec7-4c5b-4715-b83f-8bc4460b01de
content-type: text/plain
cache-control: max-age=600
last-modified: Fri, 13 Jun 2025 00:22:08 GMT
date: Fri, 13 Jun 2025 00:22:15 GMT
x-content-digest: e44ad5b6f016fa72eb8502e3d697c183b1958fed
age: 3
x-rack-cache: fresh
Content-Length: 55
</code></pre></div></div>

<p>as does the output of our Rack app (the output is essentially the same as the <code class="language-plaintext highlighter-rouge">x-rack-cache</code> header):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cache: [GET /] stale, invalid, store
cache: [GET /] fresh
</code></pre></div></div>

<p>So now we know rack-cache has used our <em>validator</em> and cached the initial version, and then served it from its cache. Neat. Let’s adjust our <code class="language-plaintext highlighter-rouge">max-age</code> a bit:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">headers</span><span class="p">[</span><span class="s2">"cache-control"</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"max-age=30"</span>
</code></pre></div></div>

<p>restart our app, and do a <code class="language-plaintext highlighter-rouge">headercheck</code> again - we changed the file, so it should pick up our changes and…</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ headercheck http://localhost:3000
HTTP/1.1 200 OK
x-request-id: a86dfec7-4c5b-4715-b83f-8bc4460b01de
content-type: text/plain
cache-control: max-age=600
last-modified: Fri, 13 Jun 2025 00:22:08 GMT
date: Fri, 13 Jun 2025 00:22:15 GMT
x-content-digest: e44ad5b6f016fa72eb8502e3d697c183b1958fed
age: 300
x-rack-cache: fresh
Content-Length: 55
</code></pre></div></div>

<p>Nope. It happily continues serving our previous version until the <code class="language-plaintext highlighter-rouge">age</code> value exceeds the <code class="language-plaintext highlighter-rouge">600</code> seconds we have set. Pudu. What we need to do in this case is actually <em>tell</em> rack-cache that it should revalidate with the <code class="language-plaintext highlighter-rouge">origin</code> every time:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">headers</span><span class="p">[</span><span class="s2">"cache-control"</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"max-age=30, must-revalidate"</span>
</code></pre></div></div>

<p>And - if we don’t want to wait another 5 minutes - we need to manually delete the cache:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rm -rf rack-cache
ruby app.rb
</code></pre></div></div>

<p>This is an important lesson (and one of the reasons “developers don’t get CDNs”).</p>

<p><strong>Lesson 1:</strong> Once you have told your caching proxies to cache something, it may be a nuisance to make them “forget” that cached data.</p>

<p>The initial request and the subsequent ones work fine:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cache: [GET /] miss, store
cache: [GET /] fresh
cache: [GET /] fresh
</code></pre></div></div>

<p>and when we change the contents of our app and restart it, we see an actual invalidation followed by <code class="language-plaintext highlighter-rouge">fresh</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cache: [GET /] stale, invalid, store
cache: [GET /] fresh
cache: [GET /] fresh
</code></pre></div></div>

<p>Once <code class="language-plaintext highlighter-rouge">max-age</code> lapses, our app again gets hit and rack-cache knows that even though <code class="language-plaintext highlighter-rouge">last-modified</code> is the same, the output has likely changed and checks anyway.</p>

<h2 id="actually-making-the-get-conditional">Actually making the GET conditional</h2>

<p>Now let’s actually use our validator. In our previous example, rack-cache would ask us for a new rendered response regardless once the <code class="language-plaintext highlighter-rouge">max-age</code> has lapsed. Now, let’s check for the <code class="language-plaintext highlighter-rouge">If-Modified-Since</code> header - and if it hasn’t changed since our file modification time - respond with a 304:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">request_id</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">uuid</span>
<span class="n">headers</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"x-request-id"</span> <span class="o">=&gt;</span> <span class="n">request_id</span><span class="p">,</span> <span class="s2">"content-type"</span> <span class="o">=&gt;</span> <span class="s2">"text/plain"</span><span class="p">}</span>
<span class="n">headers</span><span class="p">[</span><span class="s2">"cache-control"</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"max-age=30, must-revalidate"</span>
<span class="n">mtime_http</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">mtime</span><span class="p">(</span><span class="kp">__FILE__</span><span class="p">).</span><span class="nf">httpdate</span>
<span class="n">headers</span><span class="p">[</span><span class="s2">"last-modified"</span><span class="p">]</span> <span class="o">=</span> <span class="n">mtime_http</span>

<span class="k">if</span> <span class="n">env</span><span class="p">[</span><span class="s2">"HTTP_IF_MODIFIED_SINCE"</span><span class="p">]</span> <span class="o">&amp;&amp;</span> <span class="no">Time</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">env</span><span class="p">[</span><span class="s2">"HTTP_IF_MODIFIED_SINCE"</span><span class="p">]).</span><span class="nf">httpdate</span> <span class="o">==</span> <span class="n">mtime_http</span>
  <span class="p">[</span><span class="mi">304</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="p">[]]</span>
<span class="k">else</span>
  <span class="p">[</span><span class="mi">200</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="p">[</span><span class="s2">"Gruezi wieder for request </span><span class="si">#{</span><span class="n">request_id</span><span class="si">}</span><span class="s2">"</span><span class="p">]]</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now, a few requests (and having waited 30 seconds), we see that our validator is used and is actually still valid:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cache: [GET /] stale, invalid, store
cache: [GET /] fresh
cache: [GET /] fresh
cache: [GET /] fresh
cache: [GET /] fresh
cache: [GET /] fresh
cache: [GET /] fresh
cache: [GET /] fresh
cache: [GET /] fresh
cache: [GET /] fresh
cache: [GET /] fresh
cache: [GET /] stale, valid, store
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ headercheck http://localhost:3000
HTTP/1.1 200 OK
x-request-id: a12dbf44-41ef-4c92-a926-1daccf6749c9
content-type: text/plain
cache-control: max-age=30, must-revalidate
last-modified: Fri, 13 Jun 2025 00:38:09 GMT
date: Fri, 13 Jun 2025 00:38:17 GMT
x-content-digest: 900b1529121c74f438ae26ba4b24c0ebc858cf04
age: 27
x-rack-cache: fresh
Content-Length: 62

$ headercheck http://localhost:3000
HTTP/1.1 200 OK
x-request-id: a12dbf44-41ef-4c92-a926-1daccf6749c9
content-type: text/plain
cache-control: max-age=30, must-revalidate
last-modified: Fri, 13 Jun 2025 00:38:09 GMT
x-content-digest: 900b1529121c74f438ae26ba4b24c0ebc858cf04
date: Fri, 13 Jun 2025 00:38:50 GMT
age: 0
x-rack-cache: stale, valid, store
Content-Length: 62
</code></pre></div></div>

<p>Note how, in the second request, while its <code class="language-plaintext highlighter-rouge">age</code> is reset to 0, the <code class="language-plaintext highlighter-rouge">x-request-id</code> header is exactly the same - and so is the content-digest! rack-cache resets the age, but it reuses the already cached response and does not call our application again.</p>

<h2 id="etags">ETags</h2>

<p>A <code class="language-plaintext highlighter-rouge">last-modified</code> has a fundamental limitation. It is a low-resolution timestamp (only to a second). If our resource changes multiple times per second, the changes will not register - even though the contents of the resource may have changed. But more importantly - a <code class="language-plaintext highlighter-rouge">last-modified</code> does not provide us with a fundamental “webmastery” thing we would want at all times: <strong>read after write consistency</strong></p>

<p>When you have a page, and you have some kind of CMS that renders that page, when you edit the page you <em>absolutely want</em> to see your changes immediately as you hit “Publish”. But if you do that frequently, your <code class="language-plaintext highlighter-rouge">last-modified</code> time stays the same. It is wall clock, not a <a href="https://en.wikipedia.org/wiki/Vector_clock">vector clock.</a></p>

<p>This is why ETags are better. ETags are “freeform checksums” - values that only your application knows the semantics of. For example, you can have an ETag which says “version-1” and then gets incremented to “version-2”, “version-3” and so forth, effectively embedding a vector clock in the etag.</p>

<p>For reasons™ ETags must be in quotes. Let’s replace our modification time of the file with its checksum:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">headers</span><span class="p">[</span><span class="s2">"cache-control"</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"max-age=30, must-revalidate"</span>
<span class="n">etag</span> <span class="o">=</span> <span class="no">Digest</span><span class="o">::</span><span class="no">SHA1</span><span class="p">.</span><span class="nf">base64digest</span><span class="p">(</span><span class="no">File</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="kp">__FILE__</span><span class="p">)).</span><span class="nf">inspect</span>
<span class="n">headers</span><span class="p">[</span><span class="s2">"etag"</span><span class="p">]</span> <span class="o">=</span> <span class="n">etag</span>

<span class="k">if</span> <span class="n">env</span><span class="p">[</span><span class="s2">"HTTP_IF_NONE_MATCH"</span><span class="p">]</span> <span class="o">==</span> <span class="n">etag</span>
  <span class="p">[</span><span class="mi">304</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="p">[]]</span>
<span class="k">else</span>
  <span class="p">[</span><span class="mi">200</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="p">[</span><span class="s2">"Gruezi wieder for request </span><span class="si">#{</span><span class="n">request_id</span><span class="si">}</span><span class="s2">"</span><span class="p">]]</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ headercheck http://localhost:3000
HTTP/1.1 200 OK
x-request-id: da28d4c7-c9dd-43e3-a855-0183808ef640
content-type: text/plain
cache-control: max-age=30, must-revalidate
etag: "WCWGtNQ5wtRieI0Z1A4bpSOag2s="
date: Fri, 13 Jun 2025 00:48:02 GMT
x-content-digest: 5f34552c880e4bf6a7938a74b5459dfcbe0f6e0e
age: 4
x-rack-cache: fresh
Content-Length: 62
</code></pre></div></div>

<p>And after some time:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ headercheck http://localhost:3000
HTTP/1.1 200 OK
x-request-id: da28d4c7-c9dd-43e3-a855-0183808ef640
content-type: text/plain
cache-control: max-age=30, must-revalidate
etag: "WCWGtNQ5wtRieI0Z1A4bpSOag2s="
x-content-digest: 5f34552c880e4bf6a7938a74b5459dfcbe0f6e0e
date: Fri, 13 Jun 2025 00:48:58 GMT
age: 0
x-rack-cache: stale, valid, store
Content-Length: 62
</code></pre></div></div>

<p>Notice something peculiar? Even though rack-cache is clearly reusing an already stored response, it prints <code class="language-plaintext highlighter-rouge">store</code> to the log - as if it “stores” the new response. Therefore: lesson 2.</p>

<p><strong>Lesson 2:</strong> debug information from caching proxies can be very confusing.</p>

<h2 id="actually-conditional-gets">Actually conditional GETs</h2>

<p>Now let’s get to the meat of the matter: if you want true “read-after-write” consistency, here is what you need to do:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">headers</span><span class="p">[</span><span class="s2">"cache-control"</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"must-revalidate"</span>
</code></pre></div></div>

<p>Yes, you <em>remove</em> <code class="language-plaintext highlighter-rouge">max-age</code> entirely. It means that every request will end up with our <code class="language-plaintext highlighter-rouge">origin</code> app and may cause some computation, but if it is cheap enough - we would just confirm the validator and respond with a 304.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ headercheck http://localhost:3000
HTTP/1.1 200 OK
x-request-id: 831f88d1-9873-4241-8356-e80059854c73
content-type: text/plain
cache-control: must-revalidate
etag: "yxMkFgyIcrJBk8AaIxDOYdWq0VU="
x-content-digest: ba6bad0df3cb83cebf87a37d1a85ca59e73c7c85
date: Fri, 13 Jun 2025 00:53:53 GMT
age: 0
x-rack-cache: stale, valid, store
Content-Length: 62

$ headercheck http://localhost:3000
HTTP/1.1 200 OK
x-request-id: 831f88d1-9873-4241-8356-e80059854c73
content-type: text/plain
cache-control: must-revalidate
etag: "yxMkFgyIcrJBk8AaIxDOYdWq0VU="
x-content-digest: ba6bad0df3cb83cebf87a37d1a85ca59e73c7c85
date: Fri, 13 Jun 2025 00:53:54 GMT
age: 0
x-rack-cache: stale, valid, store
Content-Length: 62
</code></pre></div></div>

<p>If you edit the file and restart the server - the etag will change, and every subsequent revalidation will not have a matching ETag.</p>

<h2 id="the-subtle-art-of-etag-divination">The subtle art of ETag divination</h2>

<p>It is a bit of an art to come up with an ETag which accurately reflects the state of the resource displayed, but also of the application at large. For example, changes in your gems will likely lead to changes in rendered output. Changes in the objects you fetch into the view will, as well. And if your resource contains <em>lists of things</em> - the disappearance of an item from a list should <em>also</em> change the ETag.</p>

<p>Rails does provide something called <code class="language-plaintext highlighter-rouge">cache_key</code> which is available on all ActiveRecord models. The problem is that this <code class="language-plaintext highlighter-rouge">cache_key</code> can’t be used as the ETag, because it contains… the <code class="language-plaintext highlighter-rouge">updated_at</code> - and a truncated one at that. So it is actually just a <code class="language-plaintext highlighter-rouge">Last-Modified</code> supplemented with the model class name and ID:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User.first.cache_key #=&gt; "users/1"
</code></pre></div></div>

<p>And that’s… meh. It does help with identifying queries, luckily:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">User</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="s2">"id &gt; 1"</span><span class="p">).</span><span class="nf">cache_key</span> <span class="c1">#=&gt; "users/query-988b60a2f77c74ce4972ac63a82a82f4"</span>
</code></pre></div></div>

<p>but is not really usable as ETag either. It is, after all, a digest of the <em>query,</em> but not of what the query produces. The way I approach it is heavy-handed but (in my opinion) very effective:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">etag</span> <span class="o">=</span> <span class="no">Marshal</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="nf">attributes</span><span class="p">)</span>
</code></pre></div></div>

<p>If you want to have multiple values contribute to the ETag, a good tool for this is <code class="language-plaintext highlighter-rouge">Digest</code>, used in its stateful form:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">d</span> <span class="o">=</span> <span class="no">Digest</span><span class="o">::</span><span class="no">SHA1</span><span class="p">.</span><span class="nf">new</span>
<span class="n">models_involved</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">m</span><span class="o">|</span>
  <span class="n">d</span> <span class="o">&lt;&lt;</span> <span class="no">Marshal</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">d</span> <span class="o">&lt;&lt;</span> <span class="no">File</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="kp">__FILE__</span><span class="p">)</span>
<span class="n">etag</span> <span class="o">=</span> <span class="n">d</span><span class="p">.</span><span class="nf">base64digest</span><span class="p">.</span><span class="nf">inspect</span>
</code></pre></div></div>

<p>Like any other cache key, ETags require finesse. For example, imagine your ETag is for a page with a list of widgets, and the URL already specifies that we will filter down to <code class="language-plaintext highlighter-rouge">?state=shipped</code> widgets. You may then have an ActiveRecord relation for those widgets:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">user</span><span class="p">.</span><span class="nf">widgets</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">state: </span><span class="s2">"shipped"</span><span class="p">)</span><span class="sb">`
</span></code></pre></div></div>

<p>But just the query is not enough. There are two possible cases where our resource (the page, API response etc.) will change:</p>

<ul>
  <li>A widget is no longer <code class="language-plaintext highlighter-rouge">shipped</code> or a widget has become <code class="language-plaintext highlighter-rouge">shipped</code> since we computed our ETag</li>
  <li>A widget has been deleted entirely even though it was <code class="language-plaintext highlighter-rouge">shipped</code></li>
</ul>

<p>If we show all our widgets on the page, but may want to avoid rendering our view - and still produce a good ETag - we can do this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">shipped_widget_ids</span> <span class="o">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">widgets</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">state: </span><span class="s2">"shipped"</span><span class="p">).</span><span class="nf">pluck</span><span class="p">(</span><span class="ss">:id</span><span class="p">).</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">[</span><span class="ss">:id</span><span class="p">]</span> <span class="p">}</span>
<span class="n">etag</span> <span class="o">=</span> <span class="no">Digest</span><span class="o">::</span><span class="no">SHA1</span><span class="p">.</span><span class="nf">hexdigest</span><span class="p">(</span><span class="n">shipped_widget_ids</span><span class="p">.</span><span class="nf">join</span><span class="p">).</span><span class="nf">base64digest</span>
</code></pre></div></div>

<p>This captures collection membership accurately.</p>

<h2 id="shortcutting-etag-divination-even-more">Shortcutting ETag divination even more</h2>

<p>I’ve lately enjoyed SQLite a great deal. One of the advantages with SQLite is that it does have a very good <code class="language-plaintext highlighter-rouge">Last-Modified</code> - the actual <code class="language-plaintext highlighter-rouge">mtime</code> of your entire database! Combined with the Git SHA (<code class="language-plaintext highlighter-rouge">APP_REVESION</code>) it allows for great ETags - while those won’t be “read-after-write” consistent, they will be quite stable.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">d</span> <span class="o">=</span> <span class="no">Digest</span><span class="o">::</span><span class="no">SHA1</span><span class="p">.</span><span class="nf">new</span>
<span class="n">d</span> <span class="o">&lt;&lt;</span> <span class="no">ENV</span><span class="p">[</span><span class="s2">"APP_REVISION"</span><span class="p">].</span><span class="nf">to_s</span>
<span class="n">d</span> <span class="o">&lt;&lt;</span> <span class="no">File</span><span class="p">.</span><span class="nf">mtime</span><span class="p">(</span><span class="s2">"storage/production.sqlite3"</span><span class="p">).</span><span class="nf">iso8601</span>
</code></pre></div></div>

<p>This will give you a very reasonable ETag for the entire website.</p>

<h2 id="expiring-your-cache">Expiring your cache</h2>

<p>It not only happens sometimes - it <em>will</em> happen to you when trying to make your site cacheable. Your caches will become stale, the site will get updates - but the visitors will not be getting the latest version, or - which is more likely - you will bodge the ETag derivation and miss something. It is a subtle art, and <a href="https://en.wikipedia.org/wiki/Cache_invalidation">cache invalidation is one of the hard problems of computer science</a> – no biggie.</p>

<p>With the strategy I’ve described above, you don’t need to reach for that big “Expire all the things” button in your CDN admin panel. All you need to is this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">digest_for_etag</span> <span class="o">&lt;&lt;</span> <span class="s2">"please expire"</span>
</code></pre></div></div>

<p>or, since I bake the Git SHA into every Docker image I create:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">digest_for_etag</span> <span class="o">&lt;&lt;</span> <span class="no">ENV</span><span class="p">[</span><span class="s2">"APP_REVISION"</span><span class="p">]</span>
</code></pre></div></div>

<p>This way, all of your caches will automatically invalidate on every deploy. How? Well…</p>

<ul>
  <li>Since your <code class="language-plaintext highlighter-rouge">Cache-Control</code> does not specify <code class="language-plaintext highlighter-rouge">max-age</code>, the <code class="language-plaintext highlighter-rouge">max-age</code> is considered to be 0, so…</li>
  <li>…the caching proxies will always revalidate - since you said <code class="language-plaintext highlighter-rouge">must-revalidate</code>…</li>
  <li>…so they will always do a conditional GET, which…</li>
  <li>…will find a changed ETag and therefore refresh the cached resources.</li>
</ul>

<p>As Tobi Lütke <a href="https://web.archive.org/web/20110318173216/http://blog.leetsoft.com/2007/5/22/the-secret-to-memcached">wrote many moons ago</a> - you never want to manually expire.</p>

<p>Actually, I bet this is how the “instant expiry” on CloudFlare works too - they just bump a few bytes in your cache keys. But you can do that on your end as well!</p>

<h2 id="content-addressable-resources-and-immutability">Content-addressable resources and immutability</h2>

<p>That is something you can get with ETags and systems like Git - but you can also do this if you store some kind of checksum with every model in your system (which is a bit tricky - but doable). Imagine every model you display has some kind of <code class="language-plaintext highlighter-rouge">checksum_bytes</code> column in the database, which stores a SHA1 digest of all the attributes on save. Kind of like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">AttributeDerivedChecksum</span>
  <span class="kp">extend</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Concern</span>

  <span class="n">included</span> <span class="k">do</span>
    <span class="n">after_validation</span> <span class="ss">:update_attribute_checksum</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">update_attribute_checksum</span>
    <span class="k">return</span> <span class="k">unless</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">column_names</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="s2">"_attribute_checksum_bytes"</span><span class="p">)</span>
    <span class="n">new_value</span> <span class="o">=</span> <span class="no">Digest</span><span class="o">::</span><span class="no">SHA1</span><span class="p">.</span><span class="nf">digest</span><span class="p">(</span><span class="no">Marshal</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">attributes</span><span class="p">.</span><span class="nf">except</span><span class="p">(</span><span class="s2">"_attribute_checksum_bytes"</span><span class="p">)))</span>
    <span class="n">write_attribute</span><span class="p">(</span><span class="s2">"_attribute_checksum_bytes"</span><span class="p">,</span> <span class="n">new_value</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">cache_key</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">persisted?</span>
      <span class="k">super</span> <span class="o">+</span> <span class="s2">"/"</span> <span class="o">+</span> <span class="n">_attribute_checksum_bytes</span>
    <span class="k">else</span>
      <span class="k">super</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">serializable_hash</span><span class="p">(</span><span class="n">options</span> <span class="o">=</span> <span class="kp">nil</span><span class="p">)</span>
    <span class="n">options</span> <span class="o">=</span> <span class="k">if</span> <span class="n">options</span>
      <span class="n">options</span><span class="p">.</span><span class="nf">merge</span><span class="p">(</span><span class="ss">except: </span><span class="no">Array</span><span class="p">.</span><span class="nf">wrap</span><span class="p">(</span><span class="n">options</span><span class="p">[</span><span class="ss">:except</span><span class="p">]).</span><span class="nf">concat</span><span class="p">([</span><span class="ss">:_attribute_checksum_bytes</span><span class="p">]).</span><span class="nf">uniq</span><span class="p">)</span>
    <span class="k">else</span>
      <span class="p">{</span><span class="ss">except: </span><span class="p">[</span><span class="ss">:_attribute_checksum_bytes</span><span class="p">]}</span>
    <span class="k">end</span>
    <span class="k">super</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Then you can output the <code class="language-plaintext highlighter-rouge">_attribute_checksum_bytes</code> (converted to Base64 or other palatable string representation) as an ETag, or mix it into your ETag for invalidation:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">headers</span><span class="p">[</span><span class="s2">"etag"</span><span class="p">]</span> <span class="o">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">cache_key</span><span class="p">.</span><span class="nf">inspect</span>
</code></pre></div></div>

<p>Another measure you can use is the <a href="https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html">optimistic locking</a> which conveniently provides a <code class="language-plaintext highlighter-rouge">lock_version</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">LockVersionCacheKey</span>
  <span class="kp">extend</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Concern</span>

  <span class="k">def</span> <span class="nf">cache_key</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
    <span class="k">if</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">column_names</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">locking_column</span><span class="p">)</span>
      <span class="p">[</span><span class="k">super</span><span class="p">,</span> <span class="nb">self</span><span class="p">[</span><span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">locking_column</span><span class="p">]].</span><span class="nf">join</span><span class="p">(</span><span class="s2">"/"</span><span class="p">)</span>
    <span class="k">else</span>
      <span class="k">super</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>which would also enable easier conversion of an <code class="language-plaintext highlighter-rouge">ActiveRecord</code> into a stable cache key, and then - to an ETag.</p>

<h2 id="avoid-immutability">Avoid immutability</h2>

<p>The temptation would then also be to say (which is possible with some CDNs)</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">headers</span><span class="p">[</span><span class="s2">"cache-control"</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"immutable"</span>
</code></pre></div></div>

<p>But check this out: imagine that Mallory became one of the patrons of your website. Mallory posts a piece of <a href="">CSAM</a> on your site, which your system duly checksums, and marks “immutable”. The piece of CSAM ends up in your CDN caches, and since it’s marked “forever” the CDN is not going to “ring back” and ask whether it is still in place. It will happily continue serving it to visitors, oblivious to the fact that you have actually received a court order commanding you to erase the resource within 12 hours - which you totally did.</p>

<p>Therefore: unless it concerns some very benign, bespoke resources that you can be absolutely positive will never ever need to be deleted - and most likely you have none of those - just don’t rely on that feature. Ever.</p>

<p>Even if you are using filenames with a digest (like Rails generates for assets), or ActiveStorage blob keys (which are pseudorandom and never change) - and even if your pipeline is controlled end-to-end, if you do “too much” immutability or rely on “cache forever” semantics - you can end up with things which are still cached by your downstream proxies, at the least opportune moment.</p>

<h2 id="a-404-is-ephemeral">A 404 is ephemeral</h2>

<p>There is a reason that <code class="language-plaintext highlighter-rouge">404 Not Found</code> and <code class="language-plaintext highlighter-rouge">410 Gone</code> are different status codes. Here is a hypothetical scenario:</p>

<ul>
  <li>There is an address on your site called <code class="language-plaintext highlighter-rouge">/exciting-announcement</code>. It now returns a 404</li>
  <li>It was never used, but now the marketing department wants to use it to put out an announcement</li>
  <li>The announcement is there, and is happily present for a week or so, until…</li>
  <li>The legal department finds that the <code class="language-plaintext highlighter-rouge">/exciting-announcement</code> contains conflicting (and illegal) claims about a competitor, and thus has to be pulled <em>immediately.</em> It now returns a 404</li>
  <li>…but later on a way is found to make the announcement without disparaging the competitor. The URL is reinstated, but with different content.</li>
</ul>

<p>All of those scenarios will be ruined if your 404 gets cached by a proxy. And it <em>can</em> get cached provided that you enable caches for non-2xx responses - this is usually not the default for caching proxies, but you may want to do it sometimes because one of the ways your web app can be brought to its knees is by bombarding it with requests to non-existent pages.</p>

<ul>
  <li>Instating the page will not work as proxies that have, by accident, cached the 404 page</li>
  <li>Once the page is in place, the proxies will happily cache it even further…</li>
  <li>…so the request from legal to delete the page will not work…</li>
  <li>and so on</li>
</ul>

<p>This is what the 404 code is for. If you are dealing with user-uploaded content, do yourself a favor and design a workflow with “permadeletion” where a resource would return a 410 status. And <em>that</em> response can have cache headers and be cached, because it is one of the very few <em>truly</em> immutable responses.</p>

<p>My advice? Do cache 404s but they should have a <code class="language-plaintext highlighter-rouge">max-age</code>, and maybe not even that. And do not play with immutable or content-addressable resources until you have a firm grasp of the consequences.</p>

<h2 id="an-interesting-part-of-cdn-offerings">An interesting part of CDN offerings</h2>

<p>CloudFlare, for one, is an incredible product. I was very, very impressed at the depth and effectiveness of the package they offer. But there are also things which are… peculiar - and they kind of align across multiple CDN vendors.</p>

<p>One of the headliner features in CloudFlare used to be <a href="https://blog.cloudflare.com/edge-cache-expire-ttl-easiest-way-to-override/">Cache Settings Override</a> - which would allow you to ignore origin cache headers and set your own caching rules at the edge, effectively overriding whatever cache control headers your origin server sends.</p>

<p>Surprising, isn’t it? So a feature is explicitly in <em>not honoring</em> the <code class="language-plaintext highlighter-rouge">Cache-Control</code> headers. If you put this and Guillermo’s post together (even though Vercel and CloudFlare bump heads constantly) – you can figure out what is going on, but I will outline it just in case. There is a strong business use case which those CDN providers are addressing.</p>

<ul>
  <li>You are a large organization called AcmeCorp. You have a number of web properties, which all have become slow. Very, very slow.</li>
  <li>At properties 3, 7 and 12 the freshly formed DevOps™ Agile Transformation Team formed just 3 quarters ago has investigated performance and has experimented with <code class="language-plaintext highlighter-rouge">Cache-Control</code> to make things better.</li>
  <li>Sadly, the team was disbanded, the tech lead on it fired and all the ops work of that team got outsourced to the secondary HQ in Saarbrücken. They are in the middle of their third reorg – and no end in sight, so there is nobody who <em>owns</em> the cache configuration.</li>
  <li>Meanwhile, you get a mandate to <em>do something</em> because even the COO notices that web property number 3 has become insufferably slow. Users complain, support queues are growing.</li>
  <li>…but due to having removed all <a href="https://lethain.com/tech-lead-managers/">technical leads</a> from your part of the organization there is nobody to call Saarbrücken, get access to the repository, take ownership of the code setting the headers and fix them. Even where you are at, close to the COO.</li>
</ul>

<p>Therefore, what do you do?</p>

<ul>
  <li>One option is retooling your web property 3 into something very fancy with maximum Vercel lock in, and hoping that their magic tricks with streaming RSCs will magically make things go brr. You also need an entire team completely locked-in on all the newest React/Next.js practices.</li>
  <li>Put web property 3 behind CloudFlare, but then use the admin console to finally override those pesky bodged headers that those blokes in Saarbrücken just don’t want to fix (they are still having their meeting about whether <a href="https://www.reddit.com/r/cscareerquestionsEU/comments/18ocuab/comment/kegf964/">the title of Doktor Fachinformatiker</a> should be a requirement on their job descriptions or not), and be done with your day.</li>
</ul>

<p>Of course you choose the latter.</p>

<p>The CDNs are having a ball solving the problem of organizations either being unable to get their <code class="language-plaintext highlighter-rouge">Cache-Control</code> right, or with organizations being dysfunctional enough that they can get them right - but utterly unable to drive the change through. With this perspective in mind, Guillermo’s post reads differently, doesn’t it?</p>

<p>This is another reason why knowing your way around <code class="language-plaintext highlighter-rouge">Cache-Control</code> is imperative. Don’t be the team in Saarbrücken and don’t let yourself be sucked into the suffocating Vercel embrace.</p>

<h2 id="balancing-validations-and-read-after-write">Balancing validations and read-after-write</h2>

<p>It may be that you do want to have read-after-write only for people who actually can change the content on the page. That is actually harder to do than one would like, but very simple. All you need to do is <em>bump the URL.</em></p>

<p>Yes, that easy. Your cache key in a caching proxy is always a combination of the full URL, the values of headers mentioned in <code class="language-plaintext highlighter-rouge">Vary</code> and the validators. To force a refresh for a URL, you may get by with just this:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"/pages/{id}?b={rand}"</span><span class="nt">&gt;</span>View the updated page<span class="nt">&lt;/a&gt;</span>
</code></pre></div></div>

<p>For a site with public-facing pages I would actually not do that, though, and just speed up my revalidations enough that they become usable for every request. This is the next important lesson.</p>

<h2 id="you-want-less-config-not-more">You want less config, not more</h2>

<p>The header we have specified in the end (<code class="language-plaintext highlighter-rouge">Cache-Control: must-revalidate</code>) is actually implicitly expanded into this:</p>

<p><code class="language-plaintext highlighter-rouge">Cache-Control: public, max-age=0, no-cache, must-revalidate</code></p>

<p>And this is confusing as well, because <code class="language-plaintext highlighter-rouge">no-cache</code> actually means “do cache, but always revalidate first”.</p>

<p><strong>Lesson 3:</strong> Cache control is intricate, and you should aim to do as little of it as you can.</p>

<p>This actually applies to more things in modern web development, and it is a pity that both the modern platform teams and modern frontend teams work in the opposite direction (more configs and more systems, as far as the eyes can see).</p>

<h2 id="conclusion">Conclusion</h2>

<p>First, let’s reiterate the lessons one more time:</p>

<ul>
  <li><strong>Lesson 1:</strong> Once you have told your caching proxies to cache something, it may be a nuisance to make them “forget” that cached data.</li>
  <li><strong>Lesson 2:</strong> debug information from caching proxies can be very confusing.</li>
  <li><strong>Lesson 3:</strong> Cache control is intricate, and you should aim to do as little of it as you can.</li>
</ul>

<p>And here is how you can benefit from <code class="language-plaintext highlighter-rouge">Cache-Control</code>, today:</p>

<ul>
  <li>Use an embedded caching proxy inside your application to model the side-effects</li>
  <li>Once your embedded caching proxy is rock-solid and configured to your liking, set up a CDN</li>
  <li>Read your CDN’s documentation regarding <code class="language-plaintext highlighter-rouge">Cache-Control</code>. For example, here’s <a href="https://developers.cloudflare.com/cache/concepts/cache-control/">CloudFlare’s</a></li>
  <li>For a publicly-accessible “content” website, you likely want either <code class="language-plaintext highlighter-rouge">public, must-revalidate</code> with ETags or <code class="language-plaintext highlighter-rouge">public, max-age=30, must-revalidate</code> with ETags.</li>
</ul>

<p>While <code class="language-plaintext highlighter-rouge">Cache-Control</code> is fairly well-defined, there are still peculiarities to how CDNs treat it. For example, <code class="language-plaintext highlighter-rouge">stale-while-revalidate</code> is very handy, but it only functions in combination with <code class="language-plaintext highlighter-rouge">max-age</code> being higher than 0 on CloudFlare. <code class="language-plaintext highlighter-rouge">s-max-age</code> can be useful too. But before you reach for these - get your basic, locally-testable <code class="language-plaintext highlighter-rouge">Cache-Control</code> rock-solid.</p>

<p>Because I want your website to be fast, and I am not selling you a CDN. Serious.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[A while ago, a prominent Vercel employee (two, actually) posted to the tune of: Developers don’t get CDNs Exhibit A etc. It is often that random tweets somehow get me into a frenzy – somebody is wrong on the internet, yet again. But when I gave this a second thought, I figured that… this statement has more merit than I would have wanted it to have. It has merit because we do not know the very basics of cache control that are necessary (and there are not that many)! It does not have merit in the sense that force-prefetching all of your includes through Vercel’s magic RSC-combine will not, actually, solve all your problems. They are talking in solutions that they sell, and what they are not emphasizing is that the issue is with the “developer slaps ‘Cache-Control’” part. Moreover: as I will explain, a lot of juice can be squeezed out of you by CDN providers exactly because your cache control is not in order and they offer you tools that kind of “force” your performance back into a survivable state. With some improvement for your users, and to the detriment of your wallet. But first, let’s rewind and see what those CDNs actually do. CDNs use something called “conditional GET requests”. Conditional GET requests mean: Cache-Control. And even I, in my hubris, haven’t been using it correctly. After reviewing how it worked on a few of my own sites, I have overhauled my uses – and built up a “minimum understanding” of it which has been, to say the least, useful. So, there it is: the absolute bare minimum of Cache-Control knowledge you may need for a public, mostly-static (CMS-driven, let’s say) website. Strap in, this is going to be wild. And be mindful of one thing: I do not work for Vercel, CloudFlare, AWS or Fastly. I just like fast websites and I think you deserve to have your website go fast as well.]]></summary></entry><entry><title type="html">UI algorithms: a tiny promise queue</title><link href="https://blog.julik.nl/2025/05/a-tiny-promise-queue" rel="alternate" type="text/html" title="UI algorithms: a tiny promise queue" /><published>2025-05-20T00:00:00+00:00</published><updated>2025-05-20T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/05/a-tiny-promise-queue</id><content type="html" xml:base="https://blog.julik.nl/2025/05/a-tiny-promise-queue"><![CDATA[<p>I’ve needed this before - a couple of times, <a href="/2025/03/a-tiny-undo-stack">just like that other thing.</a> A situation where I am doing uploads using AJAX - or performing some other long-running frontend tasks, and I don’t want to overwhelm the system with all of them running at the same time. These tasks may be, in turn, triggering other tasks… you know the drill. And yet again, the published implementations such as <a href="https://www.npmjs.com/package/p-queue">p-queue</a> and <a href="https://github.com/cnwhy/promise-queue-plus">promise-queue-plus</a> and the one described <a href="https://www.codementor.io/@edafeadjekeemunotor/building-a-concurrent-promise-queue-with-javascript-1ano2eof0v">in this blog post</a> left me wondering: why do they have to be so big? And do I really have to carry an NPM dependency for something so small?</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->
<hr />

<p>Turns out - not really. It’s just that a promises queue is a delicate thing conceptually to get “just right”. Once that “just right” is well-known - it’s just a dozen lines. Oh, and don’t use <code class="language-plaintext highlighter-rouge">setTimeout</code>. I’ll explain. Let’s define what we want first: we have some long-running tasks. We assume that:</p>

<ul>
  <li>Our system has a number of async tasks, every task is wrapped into a JS <code class="language-plaintext highlighter-rouge">Promise</code></li>
  <li>We want a way to run no more than N of these tasks in parallel</li>
</ul>

<p>Why limit the number of tasks? Well… imagine you are working on a <a href="https://wetransfer.com">very high volume file upload service,</a> and you can have your users uploading thousands of files in one go. If you are not careful, you may start all of these uploads at once. A browser will, usually, restrict your requests to 6 in parallel (with HTTP 1.1 at least), but your end-users won’t be happy about it. Those uploads will saturate their entire uplink, <em>hard</em> - so they won’t be doing any Google Meet calls while their upload is running. If your tasks do something processor-intensive - like calculating checksums for the chunks being uploaded - they won’t be throttled by the browser, and you may end up consuming one whole core of the user’s CPU as well.</p>

<p>So: if you are dealing with long-running async tasks based on <em>unbounded</em> user input (N files, M rows etc…) it is generally a very good idea to use such a queue and let it deal with the concurrency aspects of it all.</p>

<p>Additionally, if you are running a small service - having something bombard it with requests may be very destructive. Consider: you are running a Puma webserver with 6 threads. Since you are on a shoestring budget, you don’t really want to get more boxes, and running multiple processes may be annoying for other reasons. You, therefore, have a hard limit on your app handling 6 requests in parallel. Upload requests may be slow because your fronting web server may not be doing much buffering. Once a browser starts doing 6 uploads, your capacity has been saturated.</p>

<p>Moreover - you render just-in-time thumbnails from that same web app. The thumbnails are for the images getting uploaded. The images appear in your uploaded files gallery immediately as the upload completes - and the browser starts trying to retrieve the thumbnails for them. But the web app is already busy processing the next 6 uploads! The thumbnails spin for a while and then display a “broken image” icon instead. Not good.</p>

<p>I find it a good default to limit those situations to 3 uploads at the same time.</p>

<h2 id="promising-yet-tricky">Promising, yet tricky</h2>

<p>A <code class="language-plaintext highlighter-rouge">Promise</code> is, in fact, a somewhat limited API. It is not quite like a <code class="language-plaintext highlighter-rouge">Task</code>. For example, we could imagine there being a <code class="language-plaintext highlighter-rouge">Task</code> presenting us the following API:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">fetchTask</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Task</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nf">doThingOne</span><span class="p">();</span>
  <span class="k">await</span> <span class="nf">doThingTwo</span><span class="p">();</span>
  <span class="k">return</span> <span class="k">await</span> <span class="nf">doThingThree</span><span class="p">();</span>
<span class="p">});</span>
<span class="c1">// and then somewhere later - for instance, from a queue</span>
<span class="nx">fetchTask</span><span class="p">.</span><span class="nf">start</span><span class="p">();</span>
</code></pre></div></div>

<p>This is not how JS promises work, though: a <code class="language-plaintext highlighter-rouge">Promise</code> begins “doing stuff” immediately as it gets instantiated. The “stuff” it is doing can be deferred using timeouts (a Promise can asynchronously call <code class="language-plaintext highlighter-rouge">resolve()</code> or <code class="language-plaintext highlighter-rouge">reject()</code> at any arbitrary point), but the promise itself is seen as “running”.</p>

<p>So, most APIs for queues of Promises use an “async start” callback function, which then has to be enqueued - not the <code class="language-plaintext highlighter-rouge">Promise</code> itself:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">queue</span><span class="p">.</span><span class="nf">push</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nf">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">http://myco.org/api/v0/entries.json</span><span class="dl">"</span><span class="p">))</span>
</code></pre></div></div>

<p>Note how we do not enqueue the return value of <code class="language-plaintext highlighter-rouge">fetch()</code> - which, by itself, is going to be a <code class="language-plaintext highlighter-rouge">Promise</code> - but a <em>function that returns a <code class="language-plaintext highlighter-rouge">Promise</code>.</em> Which - conveniently enough - can also be any <code class="language-plaintext highlighter-rouge">async</code> function, which our queue can call.</p>

<h2 id="defining-our-api">Defining our API</h2>

<p>So, let’s sketch out the API that we want:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">maxConcurrent</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">queue</span> <span class="o">=</span> <span class="nf">createQueue</span><span class="p">(</span><span class="nx">maxConcurrent</span><span class="p">);</span>
<span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">f</span> <span class="k">of</span> <span class="nx">droppedFiles</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="nx">asyncStart</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nf">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">/put-upload</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">{</span>
        <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">PUT</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">headers</span><span class="p">:</span> <span class="p">{</span><span class="dl">"</span><span class="s2">content-type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">binary/octet-stream</span><span class="dl">"</span><span class="p">},</span>
        <span class="na">body</span><span class="p">:</span> <span class="nx">f</span>
      <span class="p">});</span>
  <span class="p">}</span>
  <span class="nx">queue</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">asyncStart</span><span class="p">)</span>
<span class="p">}</span>
<span class="nx">queue</span><span class="p">.</span><span class="nx">running</span> <span class="c1">// returns at most 4</span>
</code></pre></div></div>

<p>If we need other things to happen after our task takes place, we can either let it happen in the same async start function:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">asyncStart</span> <span class="o">=</span> <span class="k">async </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">/put-upload</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span><span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">PUT</span><span class="dl">"</span><span class="p">,</span> <span class="na">body</span><span class="p">:</span> <span class="p">...});</span>
  <span class="kd">let</span> <span class="nx">thumb</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nf">json</span><span class="p">();</span>
  <span class="nx">someReactiveStore</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="dl">"</span><span class="s2">thumbUrl</span><span class="dl">"</span><span class="p">,</span> <span class="nx">thumb</span><span class="p">.</span><span class="nx">url</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>or place the subsequent async calls into the same queue:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">asyncStart</span> <span class="o">=</span> <span class="k">async </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">/put-upload</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span><span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">PUT</span><span class="dl">"</span><span class="p">,</span> <span class="na">body</span><span class="p">:</span> <span class="p">...});</span>
  <span class="nx">queue</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="k">async </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">let</span> <span class="nx">thumb</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nf">json</span><span class="p">()</span>
    <span class="nx">someReactiveStore</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="dl">"</span><span class="s2">thumbUrl</span><span class="dl">"</span><span class="p">,</span> <span class="nx">thumb</span><span class="p">.</span><span class="nx">url</span><span class="p">);</span>
  <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And here are some defining guardrails I think are important:</p>

<ul>
  <li>I want to know how many promises are currently running</li>
  <li>I do not want any timeouts - the only state change initiator for that Queue should be a Promise settling, regardless of the settlement outcome</li>
</ul>

<p>For that last one, we have a handy callback on any <code class="language-plaintext highlighter-rouge">Promise</code> called <code class="language-plaintext highlighter-rouge">finally()</code> - let’s make use of it. <code class="language-plaintext highlighter-rouge">finally</code> is also going to be the only event callback we will need to change state in our queue, conveniently.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">createQueue</span><span class="p">(</span><span class="nx">maxConcurrent</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">tasks</span> <span class="o">=</span> <span class="p">[];</span>
  <span class="kd">var</span> <span class="nx">running</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">startNext</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if </span><span class="p">(</span><span class="nx">running</span> <span class="o">&gt;=</span> <span class="nx">maxConcurrent</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">nextTask</span> <span class="o">=</span> <span class="nx">tasks</span><span class="p">.</span><span class="nf">shift</span><span class="p">();</span>
    <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">nextTask</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
    <span class="nx">running</span><span class="o">++</span><span class="p">;</span>
    <span class="nf">nextTask</span><span class="p">();</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="kd">get</span> <span class="nf">running</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">running</span><span class="p">;</span> <span class="p">},</span>
    <span class="kd">get</span> <span class="nf">pending</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">tasks</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="p">},</span>
    <span class="nf">push</span><span class="p">(</span><span class="nx">asyncStartFn</span><span class="p">)</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">task</span> <span class="o">=</span> <span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">taskPromise</span> <span class="o">=</span> <span class="nf">asyncStartFn</span><span class="p">();</span>
        <span class="nx">taskPromise</span><span class="p">.</span><span class="k">finally</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
          <span class="nf">startNext</span><span class="p">();</span>
        <span class="p">});</span>
      <span class="p">});</span>
      <span class="nx">tasks</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">task</span><span class="p">);</span>
      <span class="nf">startNext</span><span class="p">();</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And here is how we can use it:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">q</span> <span class="o">=</span> <span class="nf">createQueue</span><span class="p">(</span><span class="mi">3</span><span class="p">);</span>
<span class="nx">q</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">someFuncThatReturnsPromise</span><span class="p">);</span>
</code></pre></div></div>

<p>…and see whether anything is running:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if </span><span class="p">(</span><span class="nx">q</span><span class="p">.</span><span class="nx">running</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="s2">`Still </span><span class="p">${</span><span class="nx">q</span><span class="p">.</span><span class="nx">running</span><span class="p">}</span><span class="s2"> tasks running, </span><span class="p">${</span><span class="nx">q</span><span class="p">.</span><span class="nx">pending</span><span class="p">}</span><span class="s2"> pending`</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="adding-a-neat-touch-from-p-queue">Adding a neat touch from p-queue</h2>

<p>There is a lovely little thing in <code class="language-plaintext highlighter-rouge">p-queue</code> though: pushing into a queue also returns a <code class="language-plaintext highlighter-rouge">Promise</code>, which will resolve or reject once the queue has picked up your task and that task has resolved or rejected. From the user’s standpoint it looks like this:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="nx">queue</span><span class="p">.</span><span class="nf">add</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nf">got</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://sindresorhus.com</span><span class="dl">'</span><span class="p">));</span>
</code></pre></div></div>

<p>Let’s add this too. To accomplish it, we want to return a <code class="language-plaintext highlighter-rouge">Promise</code> that will resolve or reject once our queue gets to the task and makes the task’s <code class="language-plaintext highlighter-rouge">Promise</code> either resolve or reject:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//...</span>
<span class="nf">push</span><span class="p">(</span><span class="nx">asyncStartFn</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nc">Promise</span><span class="p">((</span><span class="nx">outerResolve</span><span class="p">,</span> <span class="nx">outerReject</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">task</span> <span class="o">=</span> <span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">taskPromise</span> <span class="o">=</span> <span class="nf">asyncStartFn</span><span class="p">();</span>
      <span class="nx">taskPromise</span><span class="p">.</span>
        <span class="nf">then</span><span class="p">(</span><span class="nx">outerResolve</span><span class="p">,</span> <span class="nx">outerReject</span><span class="p">).</span>
        <span class="k">finally</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
          <span class="nx">console</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="s2">`Finished task, running: </span><span class="p">${</span><span class="nx">running</span><span class="p">}</span><span class="s2">, queue: </span><span class="p">${</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">length</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
          <span class="nx">running</span><span class="o">--</span><span class="p">;</span>
          <span class="nf">startNext</span><span class="p">();</span>
      <span class="p">});</span>
    <span class="p">});</span>
    <span class="nx">tasks</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">task</span><span class="p">);</span>
    <span class="nf">startNext</span><span class="p">();</span>
  <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That is a bit of a callback soup for sure, but such is the nature of async JS. And then, getting back to our bulk upload use case::</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">maxConcurrent</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">queue</span> <span class="o">=</span> <span class="nf">createQueue</span><span class="p">(</span><span class="nx">maxConcurrent</span><span class="p">);</span>
<span class="k">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">f</span> <span class="k">of</span> <span class="nx">droppedFiles</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="nx">asyncStart</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nf">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">/put-upload</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">{</span>
        <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">PUT</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">headers</span><span class="p">:</span> <span class="p">{</span><span class="dl">"</span><span class="s2">content-type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">binary/octet-stream</span><span class="dl">"</span><span class="p">},</span>
        <span class="na">body</span><span class="p">:</span> <span class="nx">f</span>
      <span class="p">});</span>
  <span class="p">}</span>
  <span class="nx">queue</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">asyncPut</span><span class="p">).</span><span class="nf">then</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">displayNotice</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">f</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="s2"> upload complete.`</span><span class="p">);</span>
  <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Not that hard, and only 26 lines. My jam.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I’ve needed this before - a couple of times, just like that other thing. A situation where I am doing uploads using AJAX - or performing some other long-running frontend tasks, and I don’t want to overwhelm the system with all of them running at the same time. These tasks may be, in turn, triggering other tasks… you know the drill. And yet again, the published implementations such as p-queue and promise-queue-plus and the one described in this blog post left me wondering: why do they have to be so big? And do I really have to carry an NPM dependency for something so small?]]></summary></entry><entry><title type="html">Streamlining web app development with Zeroconf</title><link href="https://blog.julik.nl/2025/05/dev-subdomains-with-zeroconf" rel="alternate" type="text/html" title="Streamlining web app development with Zeroconf" /><published>2025-05-15T00:00:00+00:00</published><updated>2025-05-15T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/05/dev-subdomains-with-zeroconf</id><content type="html" xml:base="https://blog.julik.nl/2025/05/dev-subdomains-with-zeroconf"><![CDATA[<p>The sites which are using <a href="/2025/04/a-can-of-shardines">Shardine</a> do not only have separate data storage - they all have their own domain names. I frequently need to validate that every site is able to work correctly with the changes I am making. At <a href="https://cheddar.me">Cheddar</a> we are also using multiple domains, which is a good security practice due to CORS and CSP. Until recently I didn’t really have a good setup for developing with multiple domains, but that has changed - and the setup I ended up with works really, really well. So, let’s dive in - it could work just as well for you too!</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="the-problem-of-multiple-hostnames">The problem of multiple hostnames</h2>

<p>When you have an application (let’s assume it is a Rails application, for simplicity) and you run <code class="language-plaintext highlighter-rouge">bin/rails s</code> or <code class="language-plaintext highlighter-rouge">bin/dev</code> the app boots and binds to <code class="language-plaintext highlighter-rouge">localhost:127.0.0.1</code>. It means that it will respond on requests to your localhost IP only (regardless of which domain name is used), and it usually runs on port <code class="language-plaintext highlighter-rouge">3000</code> - which is a Rails default.</p>

<p>When you have multiple subdomains and you want to test them, the standard approach is editing your <code class="language-plaintext highlighter-rouge">/etc/hosts</code> and adding the following segment to it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>127.0.0.1  myapp-site1
127.0.0.1  myapp-site2
127.0.0.1  myapp-site3
</code></pre></div></div>

<p>You then need to flush your DNS caches and change the Puma config to bind not to <code class="language-plaintext highlighter-rouge">localhost</code> but to your link-local IP instead:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bind</span> <span class="s2">"tcp://0.0.0.0:3000"</span>
</code></pre></div></div>

<p>This is so that Puma listens not only to <code class="language-plaintext highlighter-rouge">localhost</code> but to any request that comes in to your link-local IP.</p>

<p>This approach works, but it has some disadvantages. First, you need to edit your <code class="language-plaintext highlighter-rouge">/etc/hosts</code> and add (or remove) every app’s domains that you are working on. This is annoying and wasteful. Second, you can encounter problems with <code class="language-plaintext highlighter-rouge">.local</code>, <code class="language-plaintext highlighter-rouge">.home</code> and <code class="language-plaintext highlighter-rouge">.example</code> TLDs if you use them - so you may bump into your DNS lookups taking <a href="https://superuser.com/questions/1596225/dns-resolution-delay-for-entries-in-etc-hosts">5 seconds</a></p>

<p>Also, the <code class="language-plaintext highlighter-rouge">/etc/hosts</code> approach only makes these new names resolvable on the machine the app is running on. It is fine as long as you do not want to test your site on a mobile device, for example - a very valid use case! You want to open your app on your smartphone and examine the mobile-optimized layout, for example - as well as test the JS-heavy bits that may be slow on mobile. But just resolving to localhost will not allow your device to access your workstation where the app is running!</p>

<p>You can also use <code class="language-plaintext highlighter-rouge">.lvh.me</code> which gives you a DNS resolution to <code class="language-plaintext highlighter-rouge">127.0.0.1</code> for anything you throw at it. This removes the need to edit <code class="language-plaintext highlighter-rouge">/etc/hosts</code> but introduces another big problem. Some ISPs will actually filter out any DNS responses that you receive which resolve to <code class="language-plaintext highlighter-rouge">127.0.0.1</code> - this is called <a href="https://lifehacker.com/prevent-dns-rebinding-attacks-by-adjusting-your-router-1827022291">DNS rebinding protection</a> and one of the dominant ISPs here in the Netherlands does it so heavy-handedly and badly that even if you configure your own DNS servers their responses get filtered out as well. This manifests as another severe DNS timeout when you try to access locally-bound hosts. And the only remedy for this is… adding those hosts to your <code class="language-plaintext highlighter-rouge">/ect/hosts</code>.</p>

<p>Bah.</p>

<h2 id="bending-mdns-to-your-will">Bending mDNS to your will</h2>

<p>There is a little-known solution to all of these hurdles though. The reason <code class="language-plaintext highlighter-rouge">.local</code> domains resolve so long when using <code class="language-plaintext highlighter-rouge">/etc/hosts</code> is that <code class="language-plaintext highlighter-rouge">.local</code> is actually a special TLD reserved for Zeroconf DNS (also known as “multicast DNS”, or “mDNS”). What is this beast?</p>

<p>It is a very neat tech developed by Apple - and now supported industry-wide. It used to be called Rendezvous, and then got renamed to Bonjour. Actually, it is an amalgamation of multiple technologies, but that’s too long of a post and deserves its own page. It allows machines and devices on a local network to broadcast DNS entries without having a central DNS server configured - a machine can just “emit” messages roughly of that shape:</p>

<blockquote>
  <p>📣 To whom it may concern! There is a printer available under IP <code class="language-plaintext highlighter-rouge">192.168.15.5</code>
on port 5674 and it supports the IPP printing protocol. It desires to be identified
as <code class="language-plaintext highlighter-rouge">nonbinary-fancyprinter.local</code>! Please add this entry to your local DNS resolution chain.
If someone wants to access this website, make sure they end up on that IP! 
Cheerio!</p>
</blockquote>

<p>In fact, your computer - if it is a Mac, at least - already does this. You can examine all known mDNS services using this command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ dns-sd -B _services._dns-sd._udp
Browsing for _services._dns-sd._udp
DATE: ---Thu 15 May 2025---
11:48:42.336  ...STARTING...
Timestamp     A/R    Flags  if Domain               Service Type         Instance Name
11:48:42.337  Add        3  17 .                    _tcp.local.          _companion-link
11:48:42.337  Add        3  17 .                    _udp.local.          _asquic
11:48:42.337  Add        3  17 .                    _tcp.local.          _ssh
11:48:42.337  Add        3  17 .                    _tcp.local.          _sftp-ssh
11:48:42.337  Add        3  17 .                    _tcp.local.          _airplay
11:48:42.337  Add        3  17 .                    _tcp.local.          _raop
11:48:42.337  Add        2  17 .                    _tcp.local.          _apple-mobdev2
11:48:42.552  Add        3   1 .                    _tcp.local.          _ssh
11:48:42.552  Add        3   1 .                    _tcp.local.          _sftp-ssh
11:48:42.552  Add        3   1 .                    _tcp.local.          _smb
11:48:42.552  Add        3   1 .                    _tcp.local.          _airplay
11:48:42.552  Add        3   1 .                    _tcp.local.          _raop
11:48:42.552  Add        3   1 .                    _tcp.local.          _omnistate
11:48:42.552  Add        3   1 .                    _tcp.local.          _companion-link
11:48:42.552  Add        3  17 .                    _tcp.local.          _smb
11:48:42.552  Add        3  17 .                    _tcp.local.          _omnistate
11:48:42.552  Add        3  18 .                    _tcp.local.          _ssh
11:48:42.552  Add        3  18 .                    _tcp.local.          _sftp-ssh
11:48:42.552  Add        3  18 .                    _tcp.local.          _smb
11:48:42.552  Add        3  18 .                    _tcp.local.          _airplay
11:48:42.552  Add        3  18 .                    _tcp.local.          _raop
11:48:42.552  Add        3  18 .                    _tcp.local.          _omnistate
11:48:42.552  Add        3  18 .                    _tcp.local.          _companion-link
11:48:42.552  Add        2  18 .                    _tcp.local.          _http
11:48:43.919  Add        3  17 .                    _tcp.local.          _pdl-datastream
11:48:43.919  Add        3  17 .                    _tcp.local.          _printer
11:48:43.919  Add        3  17 .                    _tcp.local.          _ipp
11:48:43.919  Add        3  17 .                    _tcp.local.          _ipps
11:48:43.919  Add        3  17 .                    _tcp.local.          _ipp-tls
11:48:43.919  Add        2  17 .                    _tcp.local.          _http
11:48:52.724  Rmv        0  17 .                    _udp.local.          _asquic
11:48:54.976  Add        2  17 .                    _tcp.local.          _remotepairing
11:49:10.951  Rmv        0  17 .                    _tcp.local.          _remotepairing
</code></pre></div></div>

<p>It is a bit tricky to query, but here is how you can see all services advertising themselves as websites:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ dns-sd -B _http._tcp
Browsing for _http._tcp
DATE: ---Thu 15 May 2025---
11:52:03.265  ...STARTING...
Timestamp     A/R    Flags  if Domain               Service Type         Instance Name
11:52:03.590  Add        2  17 local.               _http._tcp.          Brother HL-L6210DW series
</code></pre></div></div>

<p>See that printer there? This is the web UI of the printer that I use. Now, what if I told you that the same can be achieved for our Ruby app? Because if a printer is capable of telling our network it has a website - why don’t we?</p>

<p>Fun fact: few will remember but Safari used to have a separate menu (next to that <em>other</em> menu where RSS feeds used to be…) that you could use to pick a Bonjour website to visit (image courtesy of <a href="https://www.dangercove.com/news/reign-in-your-bonjour-bookmarks/">dangercove.com</a>):</p>

<p><img src="/assets/zeroconf/bonjour-bookmarks.jpg" alt="Bonjour Bookmarks" /></p>

<p>Those were great times… How about we tap into the wisdom of elders and make some use of this fancy Bonjour stuff for our web app?</p>

<h2 id="putting-things-in-motion">Putting things in motion</h2>

<p>The first stage of doing something is <em>seeing what it is that you are doing.</em> I am a simple man and often prefer GUIs to the Terminal (and I find the <code class="language-plaintext highlighter-rouge">dns-sd</code> binary output a bit obtuse), so if you are on a Mac you can follow along using <a href="https://apps.apple.com/us/app/discovery-dns-sd-browser/id1381004916?mt=12">Discovery</a></p>

<p>It shows:</p>

<p><img src="/assets/zeroconf/discovery.png" alt="Discovery" /></p>

<p>So, how do we do this Bonjouring’ from our Rails web app? Well, there is a long and fabled history to that. A long time ago a number of folks started doing amazing happenings on Ruby meetups called <a href="https://github.blog/news-insights/git-over-bonjour/">gitjouring</a> which went as follows: folks would make their Git repos on their laptops discoverable. An attendee at a conference could see that there was a repository available for pushing, could set it as one of the Git origins and live-push to it, right during a talk! Folks would also use this as an impromptu Git hosting replacement, to be used in an absolutely chaotic, marvelous peer-to-peer fashion!</p>

<p>Though I haven’t been there, I’ve read and heard about those legendary events. What does remain to us, however, are the tools built for this by the amazing <a href="https://tenderlovemaking.com">tenderlove</a> - one of which is a beautiful, frugal mDNS advertiser gem called <code class="language-plaintext highlighter-rouge">zeroconf</code>.</p>

<p>And we are going to use it to set up the following domain names for our sites. The machine we are running on has a name - usually in the <code class="language-plaintext highlighter-rouge">.local</code> domain. If you called your Mac <code class="language-plaintext highlighter-rouge">jakemac</code> its Bonjour hostname will be <code class="language-plaintext highlighter-rouge">jakemac.local</code>. This hostname is going to become our “apex” domain name.</p>

<p>Next, we will set up a subdomain with the name of our app. Since our app is called <code class="language-plaintext highlighter-rouge">cms</code>, our subdomain for the app is going to be <code class="language-plaintext highlighter-rouge">cms.jakemac.local</code>. And since our sites hosted on this app all have names - these will become the subdomains of that. So, for the 3 sites: “jane”, “peter” and “tom” we want to have the following hostnames advertised and accessible for all devices on the local network:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">jane.cms.jakemac.local</code></li>
  <li><code class="language-plaintext highlighter-rouge">peter.cms.jakemac.local</code></li>
  <li><code class="language-plaintext highlighter-rouge">tom.cms.jakemac.local</code></li>
</ul>

<p>All of these will expose our Rails app on port 3000. To start, let’s run a Zeroconf multicast from an IRB prompt - just to test the waters, as it is written in the README:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Mappe</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">001</span><span class="o">&gt;</span> <span class="nb">require</span> <span class="s2">"zeroconf"</span>
<span class="o">=&gt;</span> <span class="kp">true</span>
<span class="no">Mappe</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">002</span><span class="o">&gt;</span> <span class="no">ZeroConf</span><span class="p">.</span><span class="nf">service</span> <span class="s2">"_http._tcp.local."</span><span class="p">,</span> <span class="mi">8080</span><span class="p">,</span> <span class="s2">"test-hostname"</span>
</code></pre></div></div>

<p>The call blocks - as it spins up a thread which listens to Ethernet packets and responds to them with DNS records, and our browser shows:</p>

<p><img src="/assets/zeroconf/test-hostname.png" alt="Test Hostname" /></p>

<p>There is a peculiarity, however. If you expand the entry for the printer (ok, I have that printer - but just to make a point) you will see that the name of the device exposed (what is called “Instance name”, or “Service name”) is not the same as the domain name:</p>

<p><img src="/assets/zeroconf/instance-vs-service.png" alt="Instance Vs Service" /></p>

<p>And it does matter for us. See, the <em>service name</em> in mDNS can’t include dots, while the <em>hostname</em> that gets published - can! To make our <code class="language-plaintext highlighter-rouge">jane.cms.jakemac.local</code> available, we will need to override the instance name, as it may not include dots:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">ZeroConf</span><span class="p">.</span><span class="nf">service</span><span class="p">(</span><span class="s2">"_http._tcp.local."</span><span class="p">,</span> <span class="mi">3000</span><span class="p">,</span> <span class="s2">"jane.cms.jakemac.local"</span><span class="p">,</span> <span class="ss">instance_name: </span><span class="s2">"auto-website-1"</span><span class="p">)</span>
</code></pre></div></div>

<p>And, just like that, our automatic website appears:</p>

<p><img src="/assets/zeroconf/auto-website-appears.png" alt="Auto Website Appears" /></p>

<p>Note that you can set the hostname to whatever you want - it does not have to be a subdomain of your Mac’s hostname. I do include the name of the machine because if you happen to be working on the same codebase with fellow developers in the room - you don’t want your mDNS entries to clash with theirs. Because… if we start Discovery on a Mac connected to the same network, the site will appear there too! Your subdomain is not only available locally, but it is also available for all your devices - which is perfect for mobile testing, for example!</p>

<h2 id="productizing-it-for-your-application">Productizing it for your application</h2>

<p>Let’s create a small Rack app which can serve itself using this technique, on multiple subdomains. First, let’s set up the skeleton:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Gemfile</span>
<span class="n">source</span> <span class="s2">"https://rubygems.org"</span>

<span class="n">gem</span> <span class="s2">"rack"</span>
<span class="n">gem</span> <span class="s2">"rackup"</span>
<span class="n">gem</span> <span class="s2">"zeroconf"</span>
<span class="n">gem</span> <span class="s2">"puma"</span>
</code></pre></div></div>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config.ru</span>
<span class="n">subdomains</span> <span class="o">=</span> <span class="sx">%w( jane peter tom )</span>
<span class="n">app_name</span> <span class="o">=</span> <span class="s2">"cms"</span>
<span class="n">machine</span> <span class="o">=</span> <span class="s2">"workstation"</span>
<span class="n">port</span> <span class="o">=</span> <span class="p">(</span><span class="no">ENV</span><span class="p">[</span><span class="s2">"PORT"</span><span class="p">]</span> <span class="o">||</span> <span class="mi">9292</span><span class="p">).</span><span class="nf">to_i</span>
<span class="n">service</span> <span class="o">=</span> <span class="s2">"_http._tcp.local."</span>

<span class="nb">require</span> <span class="s2">"zeroconf"</span>
<span class="n">advertiser_threads</span> <span class="o">=</span> <span class="n">subdomains</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">subdomain</span><span class="o">|</span>
  <span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>

    <span class="n">hostname</span> <span class="o">=</span> <span class="p">[</span><span class="n">subdomain</span><span class="p">,</span> <span class="n">app_name</span><span class="p">,</span> <span class="n">machine</span><span class="p">].</span><span class="nf">join</span><span class="p">(</span><span class="s2">"."</span><span class="p">)</span> <span class="c1"># No .local here</span>
    <span class="n">instance_name_wihout_dots</span> <span class="o">=</span> <span class="p">[</span><span class="n">subdomain</span><span class="p">,</span> <span class="n">app_name</span><span class="p">,</span> <span class="n">machine</span><span class="p">].</span><span class="nf">join</span><span class="p">(</span><span class="s2">"-"</span><span class="p">)</span>
    <span class="no">ZeroConf</span><span class="p">.</span><span class="nf">service</span><span class="p">(</span><span class="n">service</span><span class="p">,</span> <span class="n">port</span><span class="p">,</span> <span class="n">hostname</span><span class="p">,</span> <span class="ss">instance_name: </span><span class="n">instance_name_wihout_dots</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>


<span class="n">run</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">body</span> <span class="o">=</span> <span class="s2">"You are visiting </span><span class="si">#{</span><span class="n">env</span><span class="p">[</span><span class="s2">"HTTP_HOST"</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span>
  <span class="p">[</span><span class="mi">200</span><span class="p">,</span> <span class="p">{},</span> <span class="p">[</span><span class="n">body</span><span class="p">]]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>and start it. Note we have to use <code class="language-plaintext highlighter-rouge">--host 0.0.0.0</code> so that the webserver listens to the outside requests, not only on loopback:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>julik@thicc zeroconf-rack-app $ bundle exec rackup --host 0.0.0.0
Bundler is using a binstub that was created for a different gem (rackup).
You should run `bundle binstub rack` to work around a system/bundle conflict.
Puma starting in single mode...
* Puma version: 6.6.0 ("Return to Forever")
* Ruby version: ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin24]
*  Min threads: 0
*  Max threads: 5
*  Environment: development
*          PID: 98971
* Listening on http://0.0.0.0:9292
Use Ctrl-C to stop
</code></pre></div></div>

<p>observe our new sites appear in Discovery:</p>

<p><img src="/assets/zeroconf/success.png" alt="Success" /></p>

<p>and visit it in Safari:</p>

<p><img src="/assets/zeroconf/safari.png" alt="Safari" /></p>

<p>Success! But it gets even better. This same hostname is now also available to all local devices on your network. Such as your smartphone:</p>

<p><img src="/assets/zeroconf/iphone.jpeg" alt="Iphone" /></p>

<p>which is perfect for local testing. It will also be available to all DNS resolvers in your tests, for example:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Mappe</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">005</span><span class="o">&gt;</span> <span class="no">Patron</span><span class="o">::</span><span class="no">Session</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="s2">"http://jane.cms.workstation.local:9292"</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="c1">#&lt;Patron::Response @status_line='HTTP/1.1 200 OK'&gt;</span>
<span class="no">Mappe</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">006</span><span class="o">&gt;</span> 
</code></pre></div></div>

<p>and your end-to-end browser tests.</p>

<h2 id="security-considerations">Security considerations</h2>

<p>Note that this is good for <em>local</em> networks where you more-or-less trust the participating devices. If you run this service on a public WiFi network someplace, it is possible that a fellow hacker will be browsing for devices and will desire to connect to your <code class="language-plaintext highlighter-rouge">site1</code> to take a peek. I find this a reasonable tradeoff - but remember, Zeroconf is a product of simpler, kinder times.</p>

<p>If you really want to avoid this, you can try doing something either with having your sites authenticated, or using some kind of tunnel, or using SSL client certificates.</p>

<h2 id="what-to-watch-out-for">What to watch out for</h2>

<p>It is a finicky solution because your network needs to pass multicast packets. Moreover, if your machine is connected to both wired- and WiFi- networks - they have to be part of the same network. The Zeroconf gem chooses the first interface that it can broadcast on by order of interfaces returned by the system. On the Mac, the first interface set via the “Service Order” configuration in your System Settings. For example, on my machine Ethernet is set as priority, and thus is the one that Zeroconf picks:</p>

<p><img src="/assets/zeroconf/iface-prio.png" alt="Iface Prio" /></p>

<p>But if you do not see your sites being advertised - the chance is that they do get advertised, just to the wrong network. And - mind the dots in the service instance name.</p>

<p>Some routers that bridge WiFi and Ethernet connections <a href="https://superuser.com/a/733115/88118">may “forget” to rebroadcast multicast packets between these connections</a> - but I haven’t seen this in the wild. Zeroconf relies on specially constructed Ethernet packets to do its job, and if those get mucked about with - bad times await.</p>

<p><code class="language-plaintext highlighter-rouge">.local</code> used to be the favorite TLD of Windows systems administrators to set as “company network”. If you are in a situation where it is configured like that, it will be sometimes set as default lookup domain in your DNS configuration and will fight with Zeroconf for the discovery - trying to make it so that the DNS queries go to your company’s DNS servers instead.</p>

<p>And, lastly, there is Rails itself: make sure to <a href="https://stackoverflow.com/questions/63888930/how-do-i-add-a-host-to-my-permitted-rails-hosts-dynamically#74257636">enable those hosts</a> in Rails’ authorized host checker.</p>

<h2 id="in-summary">In summary</h2>

<p>Using Zeroconf for development is a great, great hack in my opinion. It solves a whole bunch of problems:</p>

<ul>
  <li>It allows using subdomains just as your Ruby app expects them to work (just use <code class="language-plaintext highlighter-rouge">request.domain</code> in Rails controllers as DHH intended)</li>
  <li>It bypasses DNS rebinding protection by not-so-bright ISPs</li>
  <li>It allows you to test on local devices other than the machine where the software is running</li>
  <li>You can design your app with subdomains for key responsibilities - which avoids cookie and session sharing and enables you to set different security policies for different areas of the application. The browser will help enforce those.</li>
  <li>…and, best of all: no more <code class="language-plaintext highlighter-rouge">/etc/hosts</code> editing whatsoever.</li>
</ul>

<p>And, as usual - <a href="https://x.com/tenderlove">thank you Aaron!</a> and a Friday hug to you! And thanks to <a href="https://chadfowler.com/">Chad,</a> <a href="https://technomancy.us/colophon">Phil</a>, <a href="https://evanphx.dev/">Evan</a> and the rest of the crazy crew that brought us *jour back in the day.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[The sites which are using Shardine do not only have separate data storage - they all have their own domain names. I frequently need to validate that every site is able to work correctly with the changes I am making. At Cheddar we are also using multiple domains, which is a good security practice due to CORS and CSP. Until recently I didn’t really have a good setup for developing with multiple domains, but that has changed - and the setup I ended up with works really, really well. So, let’s dive in - it could work just as well for you too!]]></summary></entry><entry><title type="html">A can of shardines: SQLite multitenancy with Rails</title><link href="https://blog.julik.nl/2025/04/a-can-of-shardines" rel="alternate" type="text/html" title="A can of shardines: SQLite multitenancy with Rails" /><published>2025-04-25T00:00:00+00:00</published><updated>2025-04-25T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/04/a-can-of-shardines</id><content type="html" xml:base="https://blog.julik.nl/2025/04/a-can-of-shardines"><![CDATA[<p>There is a pattern I am very fond of - “one database per tenant” in web applications with multiple, isolated users. Recently, I needed to fix an application I had for a long time where this database-per-tenant multitenancy utterly broke down, because I was doing connection management wrong. Which begat the question: how do you even approach doing it right?</p>

<p>And it turns out I was not alone in this. The most popular gem for multitenancy - Apartment - which I have even used in my failed startup back in the day -
<a href="https://github.com/rails-on-services/apartment/issues/304#issuecomment-2648202324">has the issue too.</a></p>

<p>The culprit of <q>does not handle multithreading very well</q> is actually deeper. Way deeper. Doing runtime-defined multiple databases with Rails has only recently become less haphazard, and there are no tools either via gems or built-in that facilitate these flows. It has also accrued a ton of complexity, and also changes with every major Rails revision.</p>

<p><strong>TL;DR</strong> If you need to do database-per-tenant multitenancy with Rails or ActiveRecord <em>right now</em> - grab the middleware from <a href="https://gist.github.com/julik/69066f5a819ac3b38480d42c1351f8ef">this gist</a> and move on.</p>

<p>If you are curious about the genesis of this solution, strap in - we are going on a tour of a sizeable problem, and of an API of stature - the ActiveRecord connection management. 
Read on and join me on the ride! Many thanks to <a href="https://kirshatrov.com/">Kir Shatrov</a> and <a href="https://fractaledmind.com">Stephen Margheim</a> for their help in this.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="the-advantages-of-the-database-per-tenant">The advantages of the “database per tenant”</h2>

<p>If you have a tenanted application (your “tenant” is a subgraph of your data model that can function independently, and mostly references other entities from within itself), you have a number of ways to approach an architecture like that. Imagine we have a system where the tenant is a <code class="language-plaintext highlighter-rouge">Site</code>. That system is some kind of end-user-serviceable CMS, and users own multiple <code class="language-plaintext highlighter-rouge">Sites</code> and can manage pages, media and other items within a <code class="language-plaintext highlighter-rouge">Site</code>. The data model will be as follows:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Site</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">Page</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">belongs_to</span> <span class="ss">:site</span>
  <span class="n">has_many</span> <span class="ss">:media_blocks</span>
  <span class="n">has_many</span> <span class="ss">:pages</span><span class="p">,</span> <span class="ss">through: :media_blocks</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">MediaBlock</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">belongs_to</span> <span class="ss">:media_item</span>
  <span class="n">belongs_to</span> <span class="ss">:page</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">Comment</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">belongs_to</span> <span class="ss">:page</span> <span class="c1"># and thus to a Site, "through"</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">MediaItem</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">has_many</span> <span class="ss">:media_items</span>
  <span class="n">has_many</span> <span class="ss">:pages</span><span class="p">,</span> <span class="ss">through: :media_items</span>
  <span class="n">has_one_</span> <span class="ss">:site</span><span class="p">,</span> <span class="ss">through: :pages</span> <span class="c1"># since it can be reused across multiple pages</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Sites very rarely get merged together, and 99% of the data that gets created within a <code class="language-plaintext highlighter-rouge">Site</code> stays inside that <code class="language-plaintext highlighter-rouge">Site</code>, forever. Either because your system has outgrown hosting a single <code class="language-plaintext highlighter-rouge">Site</code>, or because you want to have robust isolation (you don’t want Jane to post to her <code class="language-plaintext highlighter-rouge">Site</code> only for the article to end up on Blake’s <code class="language-plaintext highlighter-rouge">Site</code> by mistake), or because your system is wildly successful and profitable, you may want to apply the following strategies:</p>

<ul>
  <li>Just like our initial model - the <code class="language-plaintext highlighter-rouge">Site</code> has an <code class="language-plaintext highlighter-rouge">id</code>, some models link to it directly, some - through others</li>
  <li>Every model gets a <code class="language-plaintext highlighter-rouge">site_id</code>. Every <code class="language-plaintext highlighter-rouge">INSERT</code>, <code class="language-plaintext highlighter-rouge">UPDATE</code> or <code class="language-plaintext highlighter-rouge">DELETE</code> then knows which <code class="language-plaintext highlighter-rouge">Site</code> a particular model makes part of - and a deletion can address the database where the site is stored. Databases will then be <em>shards</em> and store multiple <code class="language-plaintext highlighter-rouge">Sites</code>. If you decide to become a host for Slashdot, and get millions of <code class="language-plaintext highlighter-rouge">Page</code> records and bullions of <code class="language-plaintext highlighter-rouge">Comment</code> records, they will likely be extracted into a separate DB. There will be a mapping table of sorts, that will record that <code class="language-plaintext highlighter-rouge">slashdot.org</code> gets mapped to <code class="language-plaintext highlighter-rouge">db_slashdot_tenant</code> explicitly.</li>
  <li>Just like our initial model - but there is only <em>one</em> <code class="language-plaintext highlighter-rouge">Site</code> record in the entire database. All records belonging to a <code class="language-plaintext highlighter-rouge">Site</code> are stored inside that database.</li>
</ul>

<p>There are other tricks for doing sharding/multitenancy well - for example, using generated primary keys which contain the tenant ID within them - so that shards can be merged, etc.</p>

<p>But what interests us here, specifically, is the last approach - having one database per tenant. For my smaller sites, using SQLite as the database has become part and parcel. Having a DB server that you can configure easily is very good. Having a database server that you do not have to configure at all - exceptional, though. Same for backups: centralised backup is great and useful. But nothing beats an <code class="language-plaintext highlighter-rouge">rsync</code> if that’s all you need to do a backup. And the schema becomes smaller too - we can move the <code class="language-plaintext highlighter-rouge">Site</code> out of the database outright, and the rest of the models no longer needs the associations to <code class="language-plaintext highlighter-rouge">site</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Page</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">has_many</span> <span class="ss">:media_blocks</span>
  <span class="n">has_many</span> <span class="ss">:pages</span><span class="p">,</span> <span class="ss">through: :media_blocks</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">MediaBlock</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">belongs_to</span> <span class="ss">:media_item</span>
  <span class="n">belongs_to</span> <span class="ss">:page</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">Comment</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">belongs_to</span> <span class="ss">:page</span> <span class="c1"># and thus to a Site, "through"</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">MediaItem</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">has_many</span> <span class="ss">:media_blocks</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Development-wise those setups are a breeze too - if you need to debug something inside a particular <code class="language-plaintext highlighter-rouge">Site</code>, all you need to do is download this site’s data. With just one <code class="language-plaintext highlighter-rouge">scp</code> command, usually. And there are elephants in the room too:</p>

<ul>
  <li>Doing schema migrations where a migration runs on one tenant, but fails on another</li>
  <li>Accessing the same DB from multiple servers</li>
  <li>Doing backups is somewhat unorthodox - there are many ways to do it</li>
</ul>

<p>But remember: using this approach has one jarring advantage. It firmly pushes you out of the “big data” territory, and even out of “medium data” - it is “tiny data”.</p>

<blockquote>
  <p>“Data which, when stored on immediately-accessible random-read storage media of reasonable speed, does not fit under your desk” is my formal definition for “big data”, if that helps.</p>
</blockquote>

<p>Some joints were exceptionally successful doing this. <a href="https://use.expensify.com/blog/scaling-sqlite-to-4m-qps-on-a-single-server">Expensify,</a> for one, is notorious for pushing and pulling SQLite well beyond the boundaries most folks would call comfortable. I know that Autodesk’s own ShotGrid - back when it used to be Shotgun and was an independent software product - used SQLite3 pretty extensively. Along with the obligatory <code class="language-plaintext highlighter-rouge">SQLite3::BusyException</code> every now and then 😉</p>

<h2 id="why-this-is-challenging-with-rails">Why this is challenging with Rails</h2>

<p>When using SQLite3 “bare”, handling a database “open” and “close” is absolutely trivial:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">SQLite3</span><span class="o">::</span><span class="no">Database</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="s2">"site_1.sqlite3"</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">db</span><span class="o">|</span>
  <span class="n">site_title</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="nf">get_first_value</span><span class="p">(</span><span class="s2">"SELECT title FROM sites LIMIT 1"</span><span class="p">)</span>
  <span class="n">pages</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="s2">"SELECT * FROM pages "</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>If we use Rack, we just wrap this in a middleware:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
  <span class="no">SQLite3</span><span class="o">::</span><span class="no">Database</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="s2">"site_1.sqlite3"</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">db</span><span class="o">|</span>
    <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">.</span><span class="nf">merge</span><span class="p">(</span><span class="s2">"site_db"</span> <span class="o">=&gt;</span> <span class="n">db</span><span class="p">))</span>
  <span class="k">end</span>
<span class="k">end</span>  
</code></pre></div></div>

<p>But for this to work, the <code class="language-plaintext highlighter-rouge">db</code> variable - the handle to the database - has to be explicitly used for every query! ActiveRecord, however, manages the connections not through a variable you give it, but through it’s own “recollection” of what database a particular ActiveRecord <a href="https://stackoverflow.com/questions/141201/how-to-best-handle-per-model-database-connections-with-activerecord">superclass connects to:</a></p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Page</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">establish_connection</span> <span class="ss">database: </span><span class="s2">"site_1.sqlite3"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Needless to say, this code runs just once (and you don’t know exactly “when” - to which the answer is <em>at first query</em>), and is not at all designed for disconnecting and reconnecting all the time. Now, if there was a way to do this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Page</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">obtain_connection_from</span> <span class="p">{</span> <span class="n">tenancy_system</span><span class="p">.</span><span class="nf">database_config</span> <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>it would have been easier, but alas. And with the addition of connection pooling, query cache, schema cache - you are looking at a sizeable contraption of <a href="https://www.youtube.com/watch?v=LFrdqQZ8FFc">things which are put on top of other things.</a> Which is what makes this exercise so frustrating: you know something utterly trivial with a “bare” API is infuriatingly complicated when doing it through ActiveRecord. Moreover - this is one of the headliner use-cases for SQLite3, and ActiveRecord seems to make it nigh-impossible to execute.</p>

<p>How come?</p>

<h2 id="churn-inevitable">Churn, inevitable</h2>

<p>The reason for difficulties with multiple databases in Rails comes down to the history of that feature and the needs of the <em>hyperscalers</em> - the Githubs, the Shopifys and the Zendesks of the ecosystem.</p>

<p>Since I have been using Rails - and ActiveRecord - for quite a long while - here is a brief history recap:</p>

<ul>
  <li>Rails 1 already had database assignment per <code class="language-plaintext highlighter-rouge">ActiveRecord::Base</code> subclass</li>
  <li>Rails 3 added connection pooling</li>
  <li>Rails 4 added <code class="language-plaintext highlighter-rouge">connection_handling</code> (albeit - hidden)</li>
  <li>Rails 6 added <code class="language-plaintext highlighter-rouge">connected_to</code></li>
  <li>Rails 7 expanded on <code class="language-plaintext highlighter-rouge">connected_to</code> with the addition of shards (so now you have both roles and shards)</li>
</ul>

<p>The interesting part of it all is that while ActiveRecord <a href="https://github.com/rails/rails/blob/main/activerecord/examples/simple.rb">example code</a> includes snippets like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Person</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">establish_connection</span> <span class="ss">adapter: </span><span class="s2">"sqlite3"</span><span class="p">,</span> <span class="ss">database: </span><span class="s2">"foobar.db"</span>
  <span class="n">connection</span><span class="p">.</span><span class="nf">create_table</span> <span class="n">table_name</span><span class="p">,</span> <span class="ss">force: </span><span class="kp">true</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span>
    <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:name</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>the changes in how Rails handles multiple databases have led to the fact that this example code is useful only in a very small number of cases. For example: with a snippet like this, how do we <code class="language-plaintext highlighter-rouge">close_connection</code>? Or, how do we tell <code class="language-plaintext highlighter-rouge">Person</code> to connect to a different database after having done a query or two?</p>

<p>If you start looking into this, an entire world opens befor your eyes. And this world has its own taxonomy - and it is sizeable. <code class="language-plaintext highlighter-rouge">ConnectionHandling</code>, <code class="language-plaintext highlighter-rouge">DatabaseConfig</code>, <code class="language-plaintext highlighter-rouge">DatabaseConfigurations</code> (yes, plural), <code class="language-plaintext highlighter-rouge">Resolver</code>, <code class="language-plaintext highlighter-rouge">PoolConfig</code>, <code class="language-plaintext highlighter-rouge">PoolManager</code>… and all these things interact, live and breathe in a carefully managed dance. Spoiler: they can do everything we need, but we have to conduct them like a little orchestra - <em>just like so.</em></p>

<h2 id="divergent-api-design">Divergent API design</h2>

<p>The way ActiveRecord is designed - having model classes with no explicit way to tell them “through which connection” they should work for this query - means that there will always be some hidden state. It can be a global, or a class variable (which is… a glorified global) - or a thread-local, but somewhere there <em>is</em> a connection, and until recently (Rails 6, to be exact) there was no official way to tell ActiveRecord which connection to use.</p>

<p>In theory, an API like this could be realised:</p>

<p><code class="language-plaintext highlighter-rouge">Page.with_connection(tenant_db_conn).first</code></p>

<p>However, this database connection argument would then need to be provided to every call to ActiveRecord - and the API is truly vast.</p>

<p>An extra complication is that a lot of the design of AR assumes that a connection to the database (and that it is going to be <em>the</em> database) will be opened early, and then kept intact. The schema cache (letting ActiveRecord subclasses know which columns are in the tables, for example) gets loaded once. The query cache gets initialised once. Migrations get run once - and, again, they run on <em>the</em> database,</p>

<h2 id="divergent-configuration-lifecycle">Divergent configuration lifecycle</h2>

<p>If you want to build a multitenant system of small tenants using SQLite3, with a single database being allocated per tenant, your needs are not exactly in alignment with a hypothetical Shopify: they want to have <code class="language-plaintext highlighter-rouge">cluster_a</code>, <code class="language-plaintext highlighter-rouge">cluster_b</code>, <code class="language-plaintext highlighter-rouge">cluster_eu</code> and <code class="language-plaintext highlighter-rouge">cluster_us</code>, each tens of terabytes in size. You, instead, want to have <code class="language-plaintext highlighter-rouge">site_1</code>, <code class="language-plaintext highlighter-rouge">site_2</code> and so on - with some being just a few KB in size.</p>

<p>This would mean that for them, the configuration of those big clusters can be output into <code class="language-plaintext highlighter-rouge">database.yml</code> programmatically. It can be source-controlled, and follow strict and specific semantics regarding</p>

<ul>
  <li>When the file gets read</li>
  <li>Whether (and when) templating is done in it, for example - to inject credentials</li>
  <li>That all internal datastructures - such as connection pools - get initialised ahead of time</li>
  <li>Preconfigured, large clusters where data is usually sharded - using things like <code class="language-plaintext highlighter-rouge">shop_id</code> - but not segregated.</li>
</ul>

<p>None of the “big guys” from the mentioned three have true, single-database-per-tenant setups – or at least I never heard they do.</p>

<p>Most of the modern ActiveRecord infrastructure is built around those assumptions, not because the makers of the feature want to work against what “we” want - they just made different tradeoffs.</p>

<h2 id="divergent-db-engine-tradeoffs">Divergent DB engine tradeoffs</h2>

<p>Another important item is database performance. “Big” database servers are designed with some assumptions. For example, if you have a database, the engine would be interested in holding file descriptors open for that database or <code class="language-plaintext highlighter-rouge">mmap()</code>ed files from it. If the tablespaces are large - they will be cached in memory, and cached fairly aggressively. If there are indices - the engine will try to cache them in memory as well, and keep access to the files containing the index data close at hand.</p>

<p>The end result is that, at least when I was working on an Apartment-based system with MySQL 5.7, after a certain number of databases created you would start hitting file descriptor limits. Those are set low on MacOS, but it was still noticeable - and it was clear that it was a question of time (and scale) - which we haven’t hit though - before that would become an actual problem.</p>

<p>On balance, thus, a database server is optimised for <em>few large databases</em> – not for thousands of small ones. This is another reason why the approach with a static <code class="language-plaintext highlighter-rouge">database.yml</code> seemed so appealing.</p>

<p>Just check <a href="https://kirshatrov.com/posts/fast-skip-locked">this article</a> out:</p>

<blockquote>
  <p>By implementing these optimizations, I’ve seen remarkable performance improvements: single MySQL server handling 2M+ ticket reservation transactions per minute while the average latency of SELECT … FOR UPDATE SKIP LOCKED query staying under 400μS.</p>
</blockquote>

<p>This is the kind of perf those “big engines” optimise for. Not “quickly handling 120 pages within this site, which is one of 2 thousands”.</p>

<p>With SQLite3, the story is completely different. SQLite3 <em>thrives</em> with multiple small databases. Since a SQLite3 database is just a file (well, 3 files sometimes, but you get the point), it makes perfect sense to have a single database per tenant in the system. Since the system is multitenanted, a request for tenant <code class="language-plaintext highlighter-rouge">A</code> is guaranteed not to need data from tenant <code class="language-plaintext highlighter-rouge">B</code>. Moreover - when we are handling a request for tenant <code class="language-plaintext highlighter-rouge">A</code>, we don’t need any resources from <code class="language-plaintext highlighter-rouge">B</code> - so we don’t even need a connection (file handle).</p>

<p>Having smaller SQLite3 databases has more affordances - for example, it’s much faster - and more granular - to do backups on a per-tenant basis. Debugging becomes much easier - instead of doing a sophisticated sequence of <code class="language-plaintext highlighter-rouge">SELECT</code>s to extract the “slice” of data for a particular tenant, you just copy the tenant’s DB wholesale. Same for granular restore. Same for deletions - removing a tenant, even a large one, is just an <code class="language-plaintext highlighter-rouge">unlink</code> away.</p>

<p>So one of the reasons why the modern multi-DB features in Rails do not support dynamic tenant management with automatically allocated databases - in large numbers - is because, at least on the surface, only SQLite3 currently makes this pattern viable.</p>

<p>And it’s not only viable – it is <em>glorious.</em> Did you know that the way iCloud works, for example, is literally millions of isolated SQLite databases, stored <em>inside</em> of larger Cassandra databases?</p>

<h2 id="back-to-the-original-problem">Back to the original problem</h2>

<p>So, I had an app. It has been running for more than a decade now. It was initially built with static HTML with some templating getting pre-processed before server upload using <code class="language-plaintext highlighter-rouge">rsync</code> - it was a glorified static site generator, essentially. Then came the “admin” features, and the app acquired databases. From the get go, the app - which is a mini-CMS of sorts - provided every website owner a UI to edit the content of their website. Every website also has its own database. Requests between them never cross - and no <code class="language-plaintext highlighter-rouge">site_id</code> is involved anywhere in the process.</p>

<p>Initially it was based on raw ERB and some glue code. Then it got rewritten into Camping, and the database switching looked roughly like the <code class="language-plaintext highlighter-rouge">establish_connection</code> example above. This was pre-Rails-3, so no Rack middleware, no frills, no nothing.</p>

<p>With the ActiveRecord 3 upgrade came the dance of splitting code into something more appropriate, along with a changeover to Sinatra. And the connection management - which got moved into a Rack middleware - took the following shape:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
  <span class="k">begin</span>
    <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">establish_connection</span><span class="p">(</span><span class="ss">adapter: </span><span class="s1">'sqlite3'</span><span class="p">,</span> 
      <span class="ss">database: </span><span class="n">env</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s1">'site_db_path'</span><span class="p">),</span>
      <span class="ss">timeout: </span><span class="no">BUSY_TIMEOUT</span><span class="p">)</span>

    <span class="n">s</span><span class="p">,</span> <span class="n">h</span><span class="p">,</span> <span class="n">b</span> <span class="o">=</span> <span class="n">app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">.</span><span class="nf">merge</span><span class="p">(</span><span class="s1">'database'</span> <span class="o">=&gt;</span> <span class="nb">self</span><span class="p">))</span>
    <span class="n">connection_closing_body</span> <span class="o">=</span> <span class="o">::</span><span class="no">Rack</span><span class="o">::</span><span class="no">BodyProxy</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">b</span><span class="p">)</span> <span class="k">do</span>
      <span class="o">::</span><span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">clear_active_connections!</span>
    <span class="k">end</span>
    <span class="p">[</span><span class="n">s</span><span class="p">,</span> <span class="n">h</span><span class="p">,</span> <span class="n">connection_closing_body</span><span class="p">]</span>
  <span class="k">rescue</span> <span class="no">Exception</span>
    <span class="o">::</span><span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">clear_active_connections!</span>
    <span class="k">raise</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And this worked… mostly. Around the same time I have installed Sentry, but I didn’t take the habit of looking at it regularly - don’t remember the exact reasons. But, after a few years of painless operation, the app started throwing odd errors. The error that caught my eye was <code class="language-plaintext highlighter-rouge">ActiveRecord::ConnectionNotEstablished</code>, and it became more frequent the more load on the site there was. More visits - more frequent errors. Fast-forward a few years, and the error became quite frequent.</p>

<p>My assumption was that to figure out what is going on, I can better upgrade to at least ActiveRecord 6. This version is the oldest Rails version which, by virtue of Ruby version compatibilities, was already able to run on Apple Silicon - <a href="/2025/03/a-little-adventure-in-modern-frontend">which I am a proud owner of now.</a></p>

<p>Some hours later and a multitude of CoffeeScript files converted (and even more Ruby files edited) the update was complete. I tested it locally, verified everything was in good working order, and deployed the app.</p>

<p>And just 30 minutes in - <code class="language-plaintext highlighter-rouge">ConnectionNotEstablished</code>. And not only that, but 10x as frequently as before. The update hasn’t fixed the problem – in fact, it made it worse. Some experiments I did:</p>

<ul>
  <li>Allocating a separate connection pool per tenant and managing it myself</li>
  <li>Doing a connection checkout from a pool and checking it back into the pool</li>
  <li>Juju and voodoo magic</li>
</ul>

<p>Nothing worked. With a helpful hint from <a href="https://kirshatrov.com/posts">Kir</a> – who is responsible for exactly the managing of database sharding at one of the “big boys” – I got the idea that it should be possible to use the new <code class="language-plaintext highlighter-rouge">roles:</code> parameter - and a fake database configuration - to achieve this functionality.</p>

<p>My mistake was that I was trying to manage pools and connections myself, manually - while the new Rails functionality is actually geared towards Rails maging the pools and connections <em>for you.</em> So in this instance there was also… a divergent understanding of the API.</p>

<p>I was still in the paradigm that you use <code class="language-plaintext highlighter-rouge">establish_connection</code> - like in the olden days - to manage that infrastructure. But the “blessed” approach is actually to furnish Rails the connection configurations and let it handle them automatically.</p>

<h2 id="the-solution">The solution</h2>

<p><code class="language-plaintext highlighter-rouge">ActiveRecord::Base</code> now has a class method called <code class="language-plaintext highlighter-rouge">connected_to</code>. It allows you to do exactly the thing you need for a database-per-tenant setup - hop into a block with something being your “main” database. Previously, it accepted <code class="language-plaintext highlighter-rouge">database:</code> with a whole DB configuration, but now it only accepts <code class="language-plaintext highlighter-rouge">role:</code> (and - with Rails 7 and above - <code class="language-plaintext highlighter-rouge">shard:</code>). This is how you use it:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connected_to</span><span class="p">(</span><span class="ss">shard: </span><span class="s2">"sites_1"</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">site</span> <span class="o">=</span> <span class="no">Site</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">site_id</span><span class="p">)</span> <span class="c1"># Which lives on this shard, "sites_1"</span>
  <span class="n">posts</span> <span class="o">=</span> <span class="n">site</span><span class="p">.</span><span class="nf">pages</span><span class="p">.</span><span class="nf">order</span><span class="p">(</span><span class="ss">created_at: :desc</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The challenge is where the <code class="language-plaintext highlighter-rouge">sites_1</code> gets configured. Normally it would be in your <code class="language-plaintext highlighter-rouge">database.yml</code>, as per the <a href="https://guides.rubyonrails.org/active_record_multiple_databases.html">official doc:</a></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">production</span><span class="pi">:</span>
  <span class="na">primary</span><span class="pi">:</span>
    <span class="na">database</span><span class="pi">:</span> <span class="s">my_primary_database</span>
    <span class="na">adapter</span><span class="pi">:</span> <span class="s">mysql2</span>
  <span class="na">primary_replica</span><span class="pi">:</span>
    <span class="na">database</span><span class="pi">:</span> <span class="s">my_primary_database</span>
    <span class="na">adapter</span><span class="pi">:</span> <span class="s">mysql2</span>
    <span class="na">replica</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="na">primary_shard_one</span><span class="pi">:</span>
    <span class="na">database</span><span class="pi">:</span> <span class="s">my_primary_shard_one</span>
    <span class="na">adapter</span><span class="pi">:</span> <span class="s">mysql2</span>
    <span class="na">migrations_paths</span><span class="pi">:</span> <span class="s">db/migrate_shards</span>
</code></pre></div></div>

<p>But if you want to switch between tenants live - and tenants get created (and deleted!) at runtime - having this static config with cross-referencing keys is not going to fly at all. Moreover - even if you can change that “God config” – how do you force ActiveRecord to reload it? How can you tell ActiveRecord that a shard/tenant no longer exists? And how do you do it in a thread-safe manner? Does it lead to a reinitialisation of all the connection pools, or just addition-deletion?</p>

<p>The solution then becomes focused in one area: taking over from ActiveRecord in managing those connection pools and naming the <code class="language-plaintext highlighter-rouge">roles</code> and <code class="language-plaintext highlighter-rouge">shards</code> automatically. This is where the bulk of the work was, in the end. We want to convert our tenant database name/filename into a string to devise the <code class="language-plaintext highlighter-rouge">role</code> name that we can furnish to AR. For me, I only went to update to Rails 6, so I didn’t go into shards yet. If I did (and I might, eventually) - the tenant name would be the <code class="language-plaintext highlighter-rouge">shard</code>, and the <code class="language-plaintext highlighter-rouge">reading</code> and <code class="language-plaintext highlighter-rouge">writing</code> roles could be used for hosting a <code class="language-plaintext highlighter-rouge">readonly: true</code> DB connection and a writable one. However, a method I ended up using does <em>not</em> support shards even on Rails 8, so read on.</p>

<h2 id="so-how-do-we-create-those-pools">So how do we create those pools?</h2>

<p>The way to do it is to <em>query</em> ActiveRecord for whether a particular connection pool is already setup or not. If it is not - there is going to be a <code class="language-plaintext highlighter-rouge">NoConnectionPool</code> exception if you try to switch to the role/shard that doesn’t exist. But doing this via rescues is not great – the control flow becomes a bit intricate. What we can do instead is check whether there is a connection pool set up for a particular role/shard, and then connect if there are none. Note that since this manages pools - it needs to be protected by a mutex:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">MUX</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
  <span class="k">if</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection_handler</span><span class="p">.</span><span class="nf">connection_pool_list</span><span class="p">(</span><span class="n">role_name</span><span class="p">).</span><span class="nf">none?</span>
    <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection_handler</span><span class="p">.</span><span class="nf">establish_connection</span><span class="p">(</span><span class="n">database_config_hash</span><span class="p">,</span> <span class="ss">role: </span><span class="n">role_name</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>After that we can use <code class="language-plaintext highlighter-rouge">connected_to</code> - which is going to be thread-safe, fast and pretty neat:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">MUX</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
  <span class="k">if</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection_handler</span><span class="p">.</span><span class="nf">connection_pool_list</span><span class="p">(</span><span class="n">role_name</span><span class="p">).</span><span class="nf">none?</span>
    <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection_handler</span><span class="p">.</span><span class="nf">establish_connection</span><span class="p">(</span><span class="n">database_config_hash</span><span class="p">,</span> <span class="ss">role: </span><span class="n">role_name</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connected_to</span><span class="p">(</span><span class="ss">role: </span><span class="n">role_name</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">pages</span> <span class="o">=</span> <span class="no">Page</span><span class="p">.</span><span class="nf">order</span><span class="p">(</span><span class="ss">created_at: :desc</span><span class="p">).</span><span class="nf">limit</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span> <span class="c1"># Only selects from that site/tenant</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="dont-forget-about-streaming-rack-bodies">Don’t forget about streaming Rack bodies</h2>

<p>There is a small additional element we need to take care of, though: doing it correctly in Rack. To have something similar to <a href="https://github.com/rails-on-services/apartment/blob/development/lib/apartment/elevators/generic.rb">Apartment::Elevator</a> we need to do something that - again - <code class="language-plaintext highlighter-rouge">apartment</code> doesn’t do correctly. If we assume all the renders of our app are buffered, we can do this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
  <span class="n">site_name</span> <span class="o">=</span> <span class="n">env</span><span class="p">[</span><span class="s2">"SERVER_NAME"</span><span class="p">]</span>
  <span class="n">connection_config_hash</span> <span class="o">=</span> <span class="p">{</span><span class="ss">adapter: </span><span class="s2">"sqlite3"</span><span class="p">,</span> <span class="ss">database: </span><span class="s2">"sites/</span><span class="si">#{</span><span class="n">site_name</span><span class="si">}</span><span class="s2">.sqlite3"</span><span class="p">}</span>
  <span class="n">role_name</span> <span class="o">=</span> <span class="s2">"site_</span><span class="si">#{</span><span class="n">site_name</span><span class="si">}</span><span class="s2">"</span>

  <span class="c1"># Create a connection pool for that tenant if it doesn't exist</span>
  <span class="no">MUX</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
    <span class="k">if</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection_handler</span><span class="p">.</span><span class="nf">connection_pool_list</span><span class="p">(</span><span class="n">role_name</span><span class="p">).</span><span class="nf">none?</span>
      <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection_handler</span><span class="p">.</span><span class="nf">establish_connection</span><span class="p">(</span><span class="n">connection_config_hash</span><span class="p">,</span> <span class="ss">role: </span><span class="n">role_name</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
  <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connected_to</span><span class="p">(</span><span class="ss">role: </span><span class="n">role_name</span><span class="p">)</span> <span class="k">do</span>
    <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="c1"># returns [status, header, body]</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>But we are aware, of course, that Rack bodies are <em>callable</em> and <em>iterable</em> - and Rack response bodies may also just be doing SQL queries. Code that lives in the Rack response body and serves streaming data is not any less useful than the one living in the app <code class="language-plaintext highlighter-rouge">call()</code> method, even though much fewer people use it. I even worked on a <a href="https://github.com/appsignal/appsignal-ruby/pull/1037">big patch to Appsignal, my favorite APM</a> that made it report what happens inside a Rack streaming body the same way it would for the app’s <code class="language-plaintext highlighter-rouge">call</code>. The way it usually works for resource release with those bodies is this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">f</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="s2">"rb"</span><span class="p">)</span>
<span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
<span class="n">body_with_close</span> <span class="o">=</span> <span class="no">Rack</span><span class="o">::</span><span class="no">BodyProxy</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">body</span><span class="p">)</span> <span class="p">{</span> <span class="n">f</span><span class="p">.</span><span class="nf">close</span> <span class="p">}</span>
<span class="p">[</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body_with_close</span><span class="p">]</span>
</code></pre></div></div>

<p>This attaches a callback to the <code class="language-plaintext highlighter-rouge">#close</code> method of the Rack body we return, which - according to the Rack <a href="https://github.com/rack/rack/blob/main/SPEC.rdoc">SPEC</a> - <em>must</em> be called by the webserver <em>or</em> by the calling middleware.</p>

<p>Reasonably enough, the ActiveRecord API for <code class="language-plaintext highlighter-rouge">connected_to</code> only works with a block. That’s a good idea from the point of encouraging the correct (and safe) usage of a rather blunt tool. However, specifically in this case, it gets in the way. Luckily, this problem can be bypassed with judicious <a href="https://blog.appsignal.com/2018/11/27/ruby-magic-fibers-and-enumerators-in-ruby.html">application of a Fiber:</a></p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">connected_to_context_fiber</span> <span class="o">=</span> <span class="no">Fiber</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
  <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connected_to</span><span class="p">(</span><span class="ss">role: </span><span class="n">role_name</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">Fiber</span><span class="p">.</span><span class="nf">yield</span>
  <span class="k">end</span>
<span class="k">end</span>
<span class="n">connected_to_context_fiber</span><span class="p">.</span><span class="nf">resume</span>

<span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
<span class="n">body_with_close</span> <span class="o">=</span> <span class="no">Rack</span><span class="o">::</span><span class="no">BodyProxy</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">body</span><span class="p">)</span> <span class="p">{</span> <span class="n">connected_to_context_fiber_</span><span class="p">.</span><span class="nf">resume</span> <span class="p">}</span>

<span class="p">[</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body_with_close</span><span class="p">]</span>
</code></pre></div></div>

<p>And thus, our “tenant switching middleware” for ActiveRecord connection management with one database per tenant becomes:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Shardine</span>
  <span class="no">MUX</span> <span class="o">=</span> <span class="no">Mutex</span><span class="p">.</span><span class="nf">new</span>

  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">connection_config</span><span class="p">)</span>
    <span class="vi">@config</span> <span class="o">=</span> <span class="n">connection_config</span>
    <span class="vi">@role_name</span> <span class="o">=</span> <span class="n">connection_config</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="ss">:database</span><span class="p">).</span><span class="nf">to_s</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">with</span><span class="p">(</span><span class="o">&amp;</span><span class="n">blk</span><span class="p">)</span>
    <span class="c1"># Create a connection pool for that tenant if it doesn't exist</span>
    <span class="no">MUX</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
      <span class="k">if</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection_handler</span><span class="p">.</span><span class="nf">connection_pool_list</span><span class="p">(</span><span class="vi">@role_name</span><span class="p">).</span><span class="nf">none?</span>
        <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection_handler</span><span class="p">.</span><span class="nf">establish_connection</span><span class="p">(</span><span class="vi">@config</span><span class="p">,</span> <span class="ss">role: </span><span class="vi">@role_name</span><span class="p">)</span>
      <span class="k">end</span>
    <span class="k">end</span>
    <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connected_to</span><span class="p">(</span><span class="ss">role: </span><span class="vi">@role_name</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">blk</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">enter!</span>
    <span class="vi">@fiber</span> <span class="o">=</span> <span class="no">Fiber</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
      <span class="n">with</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span> <span class="p">{</span> <span class="no">Fiber</span><span class="p">.</span><span class="nf">yield</span> <span class="p">}</span>
    <span class="k">end</span>
    <span class="vi">@fiber</span><span class="p">.</span><span class="nf">resume</span>
    <span class="kp">true</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">leave!</span>
    <span class="c1"># Probably there is something in ConnectionHandling</span>
    <span class="c1"># that can be used here, but I was too lazy to look</span>
    <span class="n">to_resume</span><span class="p">,</span> <span class="vi">@fiber</span> <span class="o">=</span> <span class="vi">@fiber</span><span class="p">,</span> <span class="kp">nil</span>
    <span class="n">to_resume</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">resume</span>
  <span class="k">end</span>

  <span class="k">class</span> <span class="nc">Middleware</span>
    <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">database_config_lookup</span><span class="p">)</span>
      <span class="vi">@app</span> <span class="o">=</span> <span class="n">app</span>
      <span class="vi">@lookup</span> <span class="o">=</span> <span class="n">database_config_lookup</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
      <span class="n">connection_config</span> <span class="o">=</span> <span class="vi">@lookup</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
      <span class="n">switcher</span> <span class="o">=</span> <span class="no">TenantDatabaseSwitcher</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">connection_config</span><span class="p">)</span>
      <span class="n">did_enter</span> <span class="o">=</span> <span class="n">switcher</span><span class="p">.</span><span class="nf">enter!</span>
      <span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
      <span class="n">body_with_close</span> <span class="o">=</span> <span class="no">Rack</span><span class="o">::</span><span class="no">BodyProxy</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">body</span><span class="p">)</span> <span class="p">{</span> <span class="n">switcher</span><span class="p">.</span><span class="nf">leave!</span> <span class="p">}</span>
      <span class="p">[</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body_with_close</span><span class="p">]</span>
    <span class="k">rescue</span>
      <span class="n">switcher</span><span class="p">.</span><span class="nf">leave!</span> <span class="k">if</span> <span class="n">did_enter</span>
      <span class="k">raise</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>which we then configure in <code class="language-plaintext highlighter-rouge">config.ru</code> (or in Rails similarly) like so:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">use</span> <span class="no">Shardine</span><span class="o">::</span><span class="no">Middleware</span> <span class="k">do</span> <span class="o">|</span><span class="n">env</span><span class="o">|</span>
  <span class="n">site_name</span> <span class="o">=</span> <span class="n">env</span><span class="p">[</span><span class="s2">"SERVER_NAME"</span><span class="p">]</span>
  <span class="p">{</span><span class="ss">adapter: </span><span class="s2">"sqlite3"</span><span class="p">,</span> <span class="ss">database: </span><span class="s2">"sites/</span><span class="si">#{</span><span class="n">site_name</span><span class="si">}</span><span class="s2">.sqlite3"</span><span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And there you go - a safe and performant database-per-tenant switcher.</p>

<h2 id="an-additional-hurdle">An additional hurdle</h2>

<p>Since I was upgrading to Rails 6 - which seemed the lowest “modern” version I really had to go to - there was an extra snag. By default, when you use ActiveRecord without Rails, it gets configured “conservatively” - or, rather, not configured at all. Rails 6 has the concept of <code class="language-plaintext highlighter-rouge">legacy_connection_handling</code>. Without going into too much detail, to make this solution work that parameter must be turned off explicitly. In Rails 7 and above this parameter no longer exists.</p>

<h2 id="some-remaining-work">Some remaining work</h2>

<p>Since I initially migrated my app to ActiveRecord 6 I don’t have <code class="language-plaintext highlighter-rouge">shard</code> support yet. It would actually make perfect sense to have your “reading replica” be a <code class="language-plaintext highlighter-rouge">readonly: true</code> SQLite3 database, <a href="https://fractaledmind.github.io/2024/04/11/sqlite-on-rails-isolated-connection-pools/">as Stephen has written.</a></p>

<p>Another aspect is that there’s currently no API to remove a connection pool if a tenant gets removed from the system, which I just don’t need (my tenants don’t change as frequently).</p>

<p>Handling other contexts when you need to “step into” a Tenant can be handled similarly, either using <code class="language-plaintext highlighter-rouge">connected_to</code> or using the fiber approach.</p>

<p>And, of course, the “database per tenant” workflow is just starting and it’s only in the recent years, with product from the ONCE family specifically, where SQLite3 began to shine again - as an engine of “small data, in big numbers”.</p>

<p>May we live to see this pattern come into the spotlight, finally.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[There is a pattern I am very fond of - “one database per tenant” in web applications with multiple, isolated users. Recently, I needed to fix an application I had for a long time where this database-per-tenant multitenancy utterly broke down, because I was doing connection management wrong. Which begat the question: how do you even approach doing it right? And it turns out I was not alone in this. The most popular gem for multitenancy - Apartment - which I have even used in my failed startup back in the day - has the issue too. The culprit of does not handle multithreading very well is actually deeper. Way deeper. Doing runtime-defined multiple databases with Rails has only recently become less haphazard, and there are no tools either via gems or built-in that facilitate these flows. It has also accrued a ton of complexity, and also changes with every major Rails revision. TL;DR If you need to do database-per-tenant multitenancy with Rails or ActiveRecord right now - grab the middleware from this gist and move on. If you are curious about the genesis of this solution, strap in - we are going on a tour of a sizeable problem, and of an API of stature - the ActiveRecord connection management. Read on and join me on the ride! Many thanks to Kir Shatrov and Stephen Margheim for their help in this.]]></summary></entry><entry><title type="html">Template-Scoped CSS in Rails</title><link href="https://blog.julik.nl/2025/04/template-scoped-css-in-rails" rel="alternate" type="text/html" title="Template-Scoped CSS in Rails" /><published>2025-04-09T00:00:00+00:00</published><updated>2025-04-09T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/04/template-scoped-css-in-rails</id><content type="html" xml:base="https://blog.julik.nl/2025/04/template-scoped-css-in-rails"><![CDATA[<p>Hot on the heels of the <a href="/2025/03/a-little-adventure-in-modern-frontend">previous article</a> I was asked about my idea of having co-located CSS. Now is the time to share, so read on!</p>

<h2 id="update-2026-02-7">Update (2026-02-7)</h2>

<p>Most of the points in the article still stand, but with <code class="language-plaintext highlighter-rouge">@scope</code> <a href="https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/">now being baseline</a> you can use use it in addition - or instead - of the nested classes.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="first-co-located-styles-make-sense-period">First: Co-located styles make sense, period</h2>

<p>Let’s get this out of the way first: I do believe that component-based web app development is the right approach, and CSS styles form a part of the API of a component just as its markup and its JS do. If you have a <code class="language-plaintext highlighter-rouge">Card</code> in your application, and you use it in more than 1 place, it is absolutely right to want to do this:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- _card.html.erb --&gt;</span>
<span class="nt">&lt;style </span><span class="na">scoped-to-element-below</span><span class="nt">&gt;</span>
  <span class="nc">.component</span> <span class="p">{</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>
    <span class="nl">min-width</span><span class="p">:</span> <span class="m">240px</span><span class="p">;</span>
    <span class="nl">padding</span><span class="p">:</span> <span class="m">4px</span><span class="p">;</span>
    <span class="nl">border-radius</span><span class="p">:</span> <span class="m">4px</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="nc">.component</span> <span class="nc">.title</span> <span class="p">{</span>
    <span class="nl">font-size</span><span class="p">:</span> <span class="m">12px</span><span class="p">;</span>
  <span class="p">}</span>
<span class="nt">&lt;/style&gt;</span>
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"component"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;h2</span> <span class="na">class=</span><span class="s">"title"</span><span class="nt">&gt;</span><span class="cp">&lt;%=</span> <span class="n">some_kind_of_scope</span><span class="p">.</span><span class="nf">title</span> <span class="cp">%&gt;</span><span class="nt">&lt;/h2&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>

<p>Why so? Well, for a whole number of reasons:</p>

<ul>
  <li>Renaming or relocating your component or template should be a file rename and that’s it. No greps, no codemods, none of that bullshit - just rename the file and everything should just work</li>
  <li>It is, indeed, a huge pain to figure out how various parts of your layout will break if you touch some CSS used globally</li>
  <li>You should not care where your CSS is defined - you should care what the CSS is, and have some safety to that CSS not altering the CSS of unrelated elements on the page</li>
</ul>

<p>So: I am firmly in the camp of “localized” or “scoped” styles. Webpack, back in the day, did offer us (a terrible) solution for this: just like any resource, you could - it’s terrible, but it worked - <code class="language-plaintext highlighter-rouge">import</code> a CSS stylesheet as if it was JS. It’s an absolutely horrible idea, really, but it did kind of permit the workflow I cherish here. You would have:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>MyComponent.scss
MyComponent.js
</code></pre></div></div>

<p>and these two would work in harmony. The <code class="language-plaintext highlighter-rouge">MyComponent.js</code> would <code class="language-plaintext highlighter-rouge">import</code> the <code class="language-plaintext highlighter-rouge">MyComponent.css</code> - it would not import <code class="language-plaintext highlighter-rouge">.scss</code> because having SASS wired into this mess was <em>another</em> <a href="https://create-react-app.dev/docs/adding-a-sass-stylesheet/">lap dance you had to do</a> and which I don’t want a rerun of.</p>

<p>And then it would sorta-kinda work. And it was nice.</p>

<p>Now, apparently, Vite does have something similar in the shape of “CSS modules” - but, contrary to what one may think, these modules are neither “official” Web tech nor supported natively - it’s just a replication of the same pattern. It may work for you.</p>

<p>But I want something even better.</p>

<h2 id="co-locating-on-a-template-basis">Co-locating on a template basis</h2>

<p>What we are going to do here did exist before - it’s <a href="https://x.com/mxstbr/status/1908201327811059926">💅 styled-components</a> - just without any Node or React or Webpack. And without the “💉head injections”. I am actually perplexed why Github folks didn’t arrive at the same setup, but then again - who am I to wonder?.. Note that I don’t take <em>template nesting</em> into account here - but you could, you just need to drive the pickaxe a bit deeper (to the level where Rails computes the cache keys for its russian doll caching, which does honor template nesting).</p>

<blockquote>
  <p>⚠️ Yes, this is technically not valid HTML,
<a href="https://github.com/whatwg/html/issues/1605">because there should be a lot of arguing about all the things,</a>
but I care about what works, not about what’s correct. You can, of course, turn this into a <code class="language-plaintext highlighter-rouge">head</code> injection
if you use <code class="language-plaintext highlighter-rouge">content_for</code> - but in that case you will lose the nice property of being able to inject styles
inside partials using Hotwire - which is partly what makes this approach so neat.</p>
</blockquote>

<p>Another note: this technique may cause FUOCs (flashes of unstyled content) if your CSS is heavy - that is, if it loads assets such as fonts.</p>

<p>In Rails, every template gets cached, and it also gets compiled into an actual method. Template pre-compilation has been present in Rails for a very, very long time - Rails will read your <code class="language-plaintext highlighter-rouge">.erb</code> template, convert it into <code class="language-plaintext highlighter-rouge">concat</code> method calls with string literals, and then will create a dynamically-defined method on your <code class="language-plaintext highlighter-rouge">ActionView</code> template object. That method can then be optimized using JIT. The name of the method is going to change when your template changes. There is our identifier - the name of the method compiled from the template name.</p>

<p>Next, we are going to use a hack which was apparently avoided by styled-components. See, the common approach to <code class="language-plaintext highlighter-rouge">style</code> elements is that you can only put them in the <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code> element of your page. But browsers are, actually, <a href="https://stackoverflow.com/a/16844668/153886">just fine</a> also using <code class="language-plaintext highlighter-rouge">style</code> elements anywhere on the page - and this is exactly what we are going to use.</p>

<p>The second key element we want is <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting/Using_CSS_nesting">CSS selector nesting</a> – it used to be that you needed SASS for this, but now this feature is natively available in most modern browsers.</p>

<p>There is also the <code class="language-plaintext highlighter-rouge">scoped</code> attribute on the <code class="language-plaintext highlighter-rouge">style</code> element but we won’t need that. What we are going to do is roughly this:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;style&gt;</span>
  <span class="nc">.acef1256</span> <span class="p">{</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>
    <span class="nl">min-width</span><span class="p">:</span> <span class="m">240px</span><span class="p">;</span>
    <span class="nl">padding</span><span class="p">:</span> <span class="m">4px</span><span class="p">;</span>
    <span class="nl">border-radius</span><span class="p">:</span> <span class="m">4px</span><span class="p">;</span>

    <span class="err">.title</span> <span class="err">{</span>
      <span class="nl">font-size</span><span class="p">:</span> <span class="m">12px</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="err">}</span>
<span class="nt">&lt;/style&gt;</span>
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"acef1256"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;h2</span> <span class="na">class=</span><span class="s">"title"</span><span class="nt">&gt;</span><span class="cp">&lt;%=</span> <span class="n">title</span> <span class="cp">%&gt;</span><span class="nt">&lt;/h2&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>

<p>That is: we want an automatically-defined scoping <em>class</em> for our template (usually - a partial). We want that class to be output just once, so if we output multiple cards, we want the output to be this:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;style&gt;</span>
  <span class="nc">.acef1256</span> <span class="p">{</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>
    <span class="nl">min-width</span><span class="p">:</span> <span class="m">240px</span><span class="p">;</span>
    <span class="nl">padding</span><span class="p">:</span> <span class="m">4px</span><span class="p">;</span>
    <span class="nl">border-radius</span><span class="p">:</span> <span class="m">4px</span><span class="p">;</span>

    <span class="err">.title</span> <span class="err">{</span>
      <span class="nl">font-size</span><span class="p">:</span> <span class="m">12px</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="err">}</span>
<span class="nt">&lt;/style&gt;</span>
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"acef1256"</span> <span class="na">id=</span><span class="s">"</span><span class="cp">&lt;%=</span> <span class="n">dom_id</span><span class="p">(</span><span class="n">card1</span><span class="p">)</span> <span class="cp">%&gt;</span><span class="s">"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;h2</span> <span class="na">class=</span><span class="s">"title"</span><span class="nt">&gt;</span><span class="cp">&lt;%=</span> <span class="n">card1</span><span class="p">.</span><span class="nf">title</span> <span class="cp">%&gt;</span><span class="nt">&lt;/h2&gt;</span>
<span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"acef1256"</span> <span class="na">id=</span><span class="s">"</span><span class="cp">&lt;%=</span> <span class="n">dom_id</span><span class="p">(</span><span class="n">card2</span><span class="p">)</span> <span class="cp">%&gt;</span><span class="s">"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;h2</span> <span class="na">class=</span><span class="s">"title"</span><span class="nt">&gt;</span><span class="cp">&lt;%=</span> <span class="n">card2</span><span class="p">.</span><span class="nf">title</span> <span class="cp">%&gt;</span><span class="nt">&lt;/h2&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">acef1256</code> is going to be an automatic class name, generated from the path of our current template, along with a checksum of our template file. This will enable the following nice properties:</p>

<ul>
  <li>Our CSS will apply only within this template</li>
  <li>We don’t need to care <em>where</em> the CSS for this template is defined or whether it will affect anything else - it is always scoped to this template</li>
  <li>Whether we rename the template, or move it elsewhere, or call it using different arguments or locals - we know the style will always retain this locality.</li>
</ul>

<p>And that’s it! Not much more to it.</p>

<h2 id="scoping-our-styles">Scoping our styles</h2>

<p>It’s really way less complicated than it may seem. The only complicated bit is getting access to the metadata about the current ActionView template we are rendering. Let’s define us a helper:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ScopedStylesHelper</span>
  <span class="k">def</span> <span class="nf">css_class_for_this_template</span>
    <span class="k">raise</span> <span class="s2">"@current_template is not set, so we can't intuit the wrapping class name for this block"</span> <span class="k">unless</span> <span class="vi">@current_template</span><span class="p">.</span><span class="nf">present?</span>

    <span class="c1"># Rails computes the method name the template will be compiled into.</span>
    <span class="c1"># This method name changes with changes to the template</span>
    <span class="c1"># source, and includes the digest-like details and all the other useful bits.</span>
    <span class="n">compiled_template_method_name</span> <span class="o">=</span> <span class="vi">@current_template</span><span class="p">.</span><span class="nf">method_name</span>
    <span class="c1"># We will use the virtual path as the base for our CSS class</span>
    <span class="n">template_virtual_path</span> <span class="o">=</span> <span class="vi">@current_template</span><span class="p">.</span><span class="nf">virtual_path</span>
    <span class="n">_css_class_name</span> <span class="o">=</span> <span class="n">template_virtual_path</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/(\/|_)/</span><span class="p">,</span> <span class="s2">"-"</span><span class="p">)</span> <span class="o">+</span> <span class="s2">"-"</span> <span class="o">+</span> <span class="no">Digest</span><span class="o">::</span><span class="no">SHA1</span><span class="p">.</span><span class="nf">hexdigest</span><span class="p">(</span><span class="n">compiled_template_method_name</span><span class="p">)[</span><span class="mi">0</span><span class="o">..</span><span class="mi">3</span><span class="p">]</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">css_class_selector_for_this_template</span>
    <span class="s2">".</span><span class="si">#{</span><span class="n">css_class_for_this_template</span><span class="si">}</span><span class="s2">"</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The key item here is that we are combining the name of the template file - which could well be something like <code class="language-plaintext highlighter-rouge">_card.html.erb</code> - and the name of the precompiled method which caches that template.</p>

<p>The output, then, once we actually use our <code class="language-plaintext highlighter-rouge">css_class_for_this_template</code> helper method, will be something like this:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;style&gt;</span>
<span class="nc">.projects-show-d263</span> <span class="p">{</span>
  <span class="err">.gallery</span> <span class="err">{</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">flex</span><span class="p">;</span>
    <span class="nl">flex-direction</span><span class="p">:</span> <span class="nb">row</span><span class="p">;</span>
    <span class="nl">flex-wrap</span><span class="p">:</span> <span class="nb">wrap</span><span class="p">;</span>
  <span class="p">}</span>
<span class="err">}</span>
<span class="nt">&lt;/style&gt;</span>

<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"projects-show-d263"</span><span class="nt">&gt;</span>...<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">d263</code> is that magic part derived from the checksum of our compiled template method.</p>

<h2 id="outputting-once">Outputting once</h2>

<p>Not that hard either. We want there to be only one <code class="language-plaintext highlighter-rouge">&lt;style&gt;</code> element output, regardless of how many <code class="language-plaintext highlighter-rouge">_card.html.erb</code> partials we render. For that, we want to keep some kind of page-wide cache available:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">once_per_template</span><span class="p">(</span><span class="n">html_content</span> <span class="o">=</span> <span class="kp">nil</span><span class="p">)</span>
  <span class="k">if</span> <span class="o">!</span><span class="nb">block_given?</span> <span class="o">&amp;&amp;</span> <span class="n">html_content</span><span class="p">.</span><span class="nf">blank?</span>
    <span class="k">raise</span> <span class="s2">"You need to either pass the HTML code as the first argument or render it from the passed block"</span>
  <span class="k">end</span>
  <span class="n">already_output</span> <span class="o">=</span> <span class="n">css_class_for_this_template</span>
  <span class="vi">@per_template_outputs</span> <span class="o">||=</span> <span class="no">Set</span><span class="p">.</span><span class="nf">new</span>
  <span class="k">return</span> <span class="k">if</span> <span class="vi">@per_template_outputs</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">already_output</span><span class="p">)</span>

  <span class="c1"># https://thepugautomatic.com/2013/06/helpers/</span>
  <span class="n">capture</span> <span class="k">do</span>
    <span class="n">concat</span><span class="p">((</span><span class="nb">block_given?</span> <span class="p">?</span> <span class="k">yield</span> <span class="p">:</span> <span class="n">html_content</span><span class="p">)</span> <span class="o">+</span> <span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
  <span class="k">end</span><span class="p">.</span><span class="nf">tap</span> <span class="k">do</span>
    <span class="vi">@per_template_outputs</span> <span class="o">&lt;&lt;</span> <span class="n">already_output</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This way, regardless how many times we render a block with <code class="language-plaintext highlighter-rouge">once_per_template</code>, it is only getting output once. I find it nicer to use a generic helper method like this, because inside of its block we can put our <code class="language-plaintext highlighter-rouge">&lt;style&gt;</code> element - which is going to have proper syntax highlighting in our editor!</p>

<h2 id="putting-it-all-together">Putting it all together</h2>

<p>Having set that up, we can define our <code class="language-plaintext highlighter-rouge">style</code>:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;%=</span> <span class="n">once_per_template</span> <span class="k">do</span> <span class="cp">%&gt;</span>
  <span class="nt">&lt;style&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">css_class_selector_for_this_template</span> <span class="cp">%&gt;</span> <span class="p">{</span>
      <span class="err">.title</span> <span class="err">{</span>
        <span class="nl">color</span><span class="p">:</span> <span class="nx">red</span><span class="p">;</span>
      <span class="p">}</span>
    <span class="err">}</span>
  <span class="nt">&lt;/style&gt;</span>
<span class="cp">&lt;%</span> <span class="k">end</span> <span class="o">&gt;</span>
</code></pre></div></div>

<p>and our wrapper element:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"</span><span class="cp">&lt;%=</span> <span class="n">css_class_for_this_template</span> <span class="cp">%&gt;</span><span class="s">"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;h2</span> <span class="na">class=</span><span class="s">"title"</span><span class="nt">&gt;</span><span class="cp">&lt;%=</span> <span class="n">title</span> <span class="cp">%&gt;</span><span class="nt">&lt;/h2&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>

<p>And that’s it! Localized styles for your Rails templates and partials - with zero post-processing.</p>

<h2 id="shouldnt-that-be-a-gem">Shouldn’t that be a gem?</h2>

<p>I don’t know if it’s worth it for the 50-something lines of code it consistes of. Maybe? I already got almost a hundred gems to my name that nobody is using, and having one more - with CI and other things - might be excessive. If you think this would make a nice gem - let me know.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Hot on the heels of the previous article I was asked about my idea of having co-located CSS. Now is the time to share, so read on! Update (2026-02-7) Most of the points in the article still stand, but with @scope now being baseline you can use use it in addition - or instead - of the nested classes.]]></summary></entry><entry><title type="html">A supermarket bag and a truckload of FOMO</title><link href="https://blog.julik.nl/2025/03/a-little-adventure-in-modern-frontend" rel="alternate" type="text/html" title="A supermarket bag and a truckload of FOMO" /><published>2025-03-26T00:00:00+00:00</published><updated>2025-03-26T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/03/a-little-adventure-in-modern-frontend</id><content type="html" xml:base="https://blog.julik.nl/2025/03/a-little-adventure-in-modern-frontend"><![CDATA[<p>The day was nearing to a close. The sun has already set, but that Friday evening in Amsterdam was still warm. Unusually warm, in fact, for those late days in March – as if spring decided to bless my pilgrimage, for that pilgrimage was not jovial.</p>

<p>I was sitting at a ramen joint, sipping on the broth. To my left, a blue, crinkled supermarket shopping bag was sitting solemnly, inconspicuously.</p>

<p>Inside that bag sat a slightly used Mac Studio, which I have just purchased to be able to edit CSS of my own application.</p>

<p>By the time that evening descended upon the south of Amsterdam, I have lost 3 days of my life trying to figure out why I was unable to edit CSS.</p>

<p>But let me rewind a bit.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="act-1-it-starts-with-an-app">Act 1: it starts with an app</h2>

<p>I tend to listen to people. So when the time came to start a new app - which I haven’t done in quite some time - I knew that I would like it to be modern enough. Modern enough, even, for me to be able to delegate some work on it to others once the time comes.</p>

<p>So, while traveling, I started coding on that app - on the MacBook I carry with me normally. And one of the technologies I figured I should use - because all the cool kids do it, right? - was <a href="http://tailwindcss.com/">Tailwind.</a> At <a href="https://cheddar.me">work</a> most of our setup is already on Tailwind, with some bits and bobs still not ported - but every time I needed to edit something in the specifically CSS-heavy parts of our UI, it was a bit of a struggle. I do understand the value proposition of the library, but I have it rough with learning modern CSS “two ways” - once for what Tailwind uses for dialect, and once for what it actually does.</p>

<p>To add insult to injury, one of my respected colleagues finally got enough of my lamentations (“why does it have to be so hard”) and – rightfully – told me, that if we are all working on a system which uses a certain piece of technology – I better get in line and learn it properly.</p>

<p>Since Tailwind 4 recently came out, and we would be upgrading our work app to that, I figured I could grab it to try and muster some layout in the new side project that was starting.</p>

<p>So far - so good. I found that there was a gem for Tailwind for Rails (perplexing, though, that a Rails app using Tailwind does <em>not</em> use that gem but instead uses https://github.com/rails/cssbundling-rails), so I went with that. And… it sorta worked. I liked that Tailwind 4 does not need all this baroque config setup, I liked not having to deal with Node, and I started slowly getting to grips with this <a href="https://youtu.be/QWWPk9jrvqk?t=173">RAF banter CSS:</a></p>

<blockquote>
  <p>Min-em-4! Wee-full! Fit-contain hover beegee-teal-100!</p>
</blockquote>

<p>It was honestly OK (except that I had to keep about a dozen tabs of Tailwind docs open to do even a very basic layout). A couple of days were spent working, but I wanted to continue on my home setup - where I would have the luxury of a decent desk, 2 displays and some silence. The app needed to be ready in a few days for a customer tryout.</p>

<p>And that’s where things went south.</p>

<h2 id="act-2-sigill">Act 2: SIGILL</h2>

<p>Upon returning to Rotterdam, I checked out the code of the app onto my Trashcan Mac Pro - which is, by all accounts, one of the best Apple machines I have owned and used. After a GPU replacement it has been working super-reliably, day in, day out. It has 128GB of memory (how much is that in today’s M4 Ultra prices again?). It has more than enough cores to run things in parallel. It hosts Thunderbolt devices that I need and use without me having to replace them. It can also be disassembled without ripping adhesives, cleaned - and has helped me along through a multitude of projects and challenges.</p>

<p>The ease of doing <code class="language-plaintext highlighter-rouge">SQLite3::Database.new(":memory:")</code> would hardly be permissible without those 128 gigabytes, by the way.</p>

<p>And so I check out the app, and boot <code class="language-plaintext highlighter-rouge">bin/dev</code>. Upon which I see a curious bit of output in my terminal:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Illegal instruction: 4
</code></pre></div></div>

<p>Now, everything works fine on this app otherwise. <code class="language-plaintext highlighter-rouge">convert</code> works great. New ImageMagick did install (after recompiling half of Homebrew, but it did install fine). Rails boots. Node nodes. Databases database. Compilers compile. Why is it that Tailwind, which is a CSS preprocessor - like, for reals, this thing <em>tokenizes text</em> - would not run?</p>

<p>I start investigating, and before long I arrive at an interesting discovery. Tailwind did change majorly between 3 and 4, and not only in the way of syntax. See, it used to be a set of Node modules that you install - and a set of configs that you had to write. Tailwind 4, though, is no longer a Node module… or - rather - it still <em>is</em> a Node module, if you want it that way. But it is actually a binary! A 100MB binary (for parsing HTML and extracting class names from it).</p>

<p>And that binary has to come from some place. Upon some investigation - it turns out that the binary is, actually, a build of <a href="https://bun.sh">bun</a> and an amalgamation of all the bits and bobs that comprise Tailwind. It also turns out that Bun is very…. modern. So modern, in fact, that it requires support for <a href="https://discussions.apple.com/thread/255665503?sortBy=rank">AVX2</a> instructions if you dare to run it on an Intel CPU. Which - of course - my MacPro does not support.</p>

<p>It also turned out that Linux users did get into <a href="https://github.com/tailwindlabs/tailwindcss/issues/15181">exactly the same quagmire</a> as I did. It turns out that nobody guarantees you that your build step will run on a box which has these instructions. And Docker or not, if your processor can’t do a <code class="language-plaintext highlighter-rouge">%vpand</code> - the thing will crash. The signal I saw - “illegal instruction”, or <code class="language-plaintext highlighter-rouge">SIGILL</code> - is a sign of this happening.</p>

<p>And - it turns out - Tailwind already uses bun-baseline on Linux because a lot of people complained that they are unable to run their builds on their CI systems, on their servers - but also: on some Linux machines people own. There is little reason a 10 year old CPU should not be capable of tokenizing HTML. As a matter of fact - it does so just fine, with pretty much anything libxml-based, as it does with Nokogiri in Rails - and a plethora of other applications.</p>

<p>I then assume that just creating an build with <code class="language-plaintext highlighter-rouge">bun-baseline</code> should fix it, and I open an issue. I also wanted to try building the binary myself, upon which I discovered that, to build a module that tokenizes CSS classes, I need:</p>

<ul>
  <li>A recent Node</li>
  <li>pnpm</li>
  <li>Rust</li>
  <li>Bun build environment</li>
</ul>

<p>…and upon installing all of the above - I found that the thing just would not run, because… it could not load one of the bun-related NPM modules.</p>

<p>I assumed that this issue was a skill issue on my part, and so I decided to wait for the maintainers to release a new version just putting the flag on. At this point, I already lost 1 day to debugging this.</p>

<p>Yes, exactly when I needed to do the actual app, I was already 1 day into the yak shave of figuring out why my CSS processing system crashes with a SIGILL. Mind you: not a plugin which does some fancy video decoding. Not a particle generator. A piece of code that chews text files and parses CSS classes used in those text files.</p>

<h2 id="act-3-cdn">Act 3: CDN</h2>

<p>To be able to proceed I had to divert for an interim solution. I was already committed to Tailwind on that app, and I had some styles defined. Now, even though I do have a bachelor’s in design, doing web layout for me is still <em>hard.</em> Very hard, in fact. The number of decisions you need to make, and the number of edge cases you must be considering - like, “design engineering” is a legitimate discipline.</p>

<p>So, I figured - Tailwind generates CSS classes. Yes, for doing its thing it scans your HTML and parses out the classes, and then copies them into the CSS output. But there surely, certainly, totally should be a static CSS file that I can <em>damn link</em> into my HTML to just have the same? During development I don’t really care it will be some MBs in size, as long as it has all the classes predefined. I don’t use <code class="language-plaintext highlighter-rouge">@apply</code> as I first wanted to test the waters just using Tailwind “as is”.</p>

<p>And it turns out Tailwind has such a file available - which is called <a href="https://tailwindcss.com/docs/installation/play-cdn">PlayCDN</a>. Supposedly, you link it into your HTML file and it “just works”.</p>

<p>Sort of. Because - as it turns out - Tailwind 4, while still being “zeroconfig” - which I wholly support (frontend tools have been absolutely egregious and inconsiderate with introducing configurations left, right and center) - is not so when you want to use an amalgamated version of it.</p>

<p>See, my layout has lost borders. All borders. All of them. No amount of twiddling could give them back short of forcing an inline <code class="language-plaintext highlighter-rouge">style</code> declaration - or setting my own CSS classes on the elements. As it turns out - there is such a thing as “preflight”, which is a sophisticated form of CSS reset. For some reason that I could not quite figure out, this “preflight” is <a href="https://github.com/tailwindlabs/tailwindcss/discussions/15967">permanently enabled in the CDN variant of Tailwind</a> - and, moreover, it overrides the standard Tailwind classes.</p>

<p>Under normal use, Tailwind classes are supposed to override the preflight. That’s how CSS works. That’s how selector specificity works. Except here.</p>

<p>So - I stubbornly continued to fight the layouts, just keeping in the back of my head that “these borders will come back once this bun issue is sorted”</p>

<h2 id="act-4-all-hope-is-lost">Act 4: all hope is lost.</h2>

<p>A new release of Tailwind 4 comes out, which uses the <code class="language-plaintext highlighter-rouge">bun-baseline</code> build declaration. I immediately download it, set the path for the <code class="language-plaintext highlighter-rouge">tailwindcss-rails</code> gem, and prepare to revel in my reacquired ability of specifying CSS borders - which, for reasons of Amazing Design Technological Advancement, somehow had to be taken away by progress.</p>

<p>And what do I see in the terminal?</p>

<p>A <code class="language-plaintext highlighter-rouge">SIGILL</code> stubbornly stares at me. Using baseline <code class="language-plaintext highlighter-rouge">bun</code> did jack shit. Parsing my HTML still tries to use CPU instructions which I do not have available.</p>

<p>How could that be? Very simply, as it turns out. Bun just <em>disabled</em> compatibility with the last version of macOS that runs on my machine. Just like that. As this was done, the next logical consideration was as follows: since there are no machines without the AVX2 instruction set on the CPU that are supported by the <em>next</em> major macOS version, it is no use to have a build without those instructions!</p>

<p>But “to not break builds” the <code class="language-plaintext highlighter-rouge">baseline</code> declaration was kept in place. As an alias.</p>

<p>My computer with 128GB of RAM and 6 CPU cores was, as of summer 2024, officially not good enough to process CSS selectors.</p>

<p>At this stage I just wanted to proceed with the app, and I was sick to my stomach of having spent so much time on this issue. It did not bring me joy. It did not bring me convenience. And it did not advance my app one inch - all it gave me was a rabbit hole to dive into, head first, and a cup of frustration to drink to the very bottom.</p>

<h2 id="epilogue">Epilogue.</h2>

<p>And this is what brings me to that faithful day. I was contemplating a new computer for a while, and was even eyeing an M3 Ultra MacStudio. Only it is so that the beast is on backorder here. On a 3 week backorder, in fact - as if you are buying an nVidia 5090 on its release date. So after a quick trip to Marktplaats I decided to settle this and found a barely-used M2 machine I could purchase. Today.</p>

<p>I brought it home, installed everything, figured out everything that was utterly borked on macOS Sequoia versus Monterey I used to run, and ran my app. The borders I have coded in Tailwind looked amazing. By this time, another day was spent on setting up this new machine. Another day, thus, which was not spent on moving the app forward.</p>

<p>The borders were amazing, but I did the only thing that - at this time - was sane. And which was, in fact, suggested by a couple of folks I deeply respect.</p>

<p>With some help of a friend I ripped Tailwind out of that app for good, and I promise - unless it flies high and warrants a team working on it, it is not going to be making a comeback.</p>

<h2 id="what-it-is-actually-about">What it is actually about</h2>

<p>You may well think that I am “another grumpy Rails developer who hates modern frontend”. Or - that I am another “ageing Mac user who doesn’t want to move on with the times”. Or - “I know this guy, he never makes anything of value and just likes being angry and frustrated”. Or - <a href="https://live.julik.nl/2013/05/javascript-is-shit">he just hates JavaScript and frontend… </a></p>

<p>To this I say: <a href="/2024/02/change-my-mind">changing your mind is not my job</a> You are very welcome, nobody is forcing you to read this article - and it was a pleasure to have you visit. I am writing this not out of anger or frustration, but because this concerns something I care deeply about - being able to do our work well, in peace, without anxiety. That’s what matters.</p>

<p>The important angle here, actually, <strong>fucking matters.</strong></p>

<p>And it is this: We sometimes jump on using tech not because we <em>need it.</em> Not because we <em>want to.</em> But because we have FOMO. Either - we fear not being “current” and becoming “unhireable”, or - we fear not being “smart enough” in today’s market. One of the parts of being a good software maker is being “up on the latest” - you don’t have to use all of it, but - in the web world for sure - you need to have at least some basic understanding of how things work.</p>

<p>And sometimes, we even have colleagues who - for they don’t know better - start gently nudging us to explore if we don’t know. That nudging may, actually, turn into prodding - and then to mobbing - if you see that there is a technology choice that is not efficient, not great, and that is not a good fit - yet you have to use it anyway. <strong>Using and not loving</strong> is not considered enough.</p>

<h2 id="why-techfluencing-can-be-harmful">Why techfluencing can be harmful</h2>

<p>And then there are influencers. “If you are not using <code class="language-plaintext highlighter-rouge">$tech</code> 0 you are missing out.” “<code class="language-plaintext highlighter-rouge">$tech</code> is a game-changer - you can try to spend 10 years building half of it, badly, and won’t get nearly as good”.</p>

<p>I won’t be pointing fingers, but there is a cohort of “frontend-adjacent” influencers (at least 4) - who do this all the time.</p>

<p>And while they do get their exposure, their likes and subs and their premium followers – and some get the influx of customers, and some others - excellent senior tech evangelist positions, an important side effect is still there. They either do not understand how harmful their approach is – or they engage in it knowingly, which actually makes them culpable.</p>

<p>Because the biggest fear of many in our industry is <em>that you can become irrelevant.</em> And good software folks - ones on top of their game, whip-smart, kind, caring folks - do know that their own assessment of their relevance is likely <em>not</em> accurate. And they try to get it externally, “probe” the environment to know what’s about.</p>

<p>Who do they turn to? Their tech lead they no longer have? Their engineering manager who is waving JIRA tickets and asking questions about “5 years”? A community enthusiast who has just finished migrating his pet project from Elm to Rescript?</p>

<p>Likely not. But content from influencers is easily accessible and easily consumable. Moreover - it gets cooked in such a way as to be catchy, captivating and to sound authoritative. You don’t have to be an overt jerk to be dismissive and authoritative - the soft-spoken, passive-agressive tone, combined with a perfect haircut and great lighting, will also do the job. So will stealing other people’s ideas and capturing a community.</p>

<p>And if you consume it <em>just enough</em> you will start to second guess yourself. “Am I actually going in the right direction?” “Did I pick the right tool?” “Should I give this new fancy thing a whirl?”</p>

<p>And every junction - every decision - every infinitesimal choice you need to make when building your app - will be this second-guessing game.</p>

<p>And there is another reason for this second-guessing and for why those choices are difficult. If you have been at this game for long enough - you know that the <strong>hardest</strong> migrations and upgrades on your app are going to be the frontend upgrades. Node version changes, bundler version changes, syntax changes, changes of defaults. React router upgrades, create-react-app sunsetting, arrival-then-departure of SASS, then libsass, then Blueprint… Every choice made without proper consideration is going to have consequences.</p>

<p>And the consequences can be quite sad, because these “fad of the day” frontend tools are great when you are making apps for clients - and then throw them over the fence and forget about them. It is not going to be your problem when CSS suddenly stops working, or the React team <a href="https://t.co/khTtNHclhc">deprecates an essential feature</a> - it is going to be the problem of that poor sod who will be tasked with “figuring out how we fix this vulnerability”, three years from now. By the time he gets started on it, you will be long gone - upgrading your current project from Elm to Rescript – or some other fancy tech that is going to be fancy in 3 years.</p>

<p>But if we are following the other direction that is, apparently, the right way to go (for many reasons) - and we build something for ourselves - we look into the future - and we see that there won’t be “that poor sod” dealing with this Byzantine pyramid of dependencies. It is going to be us. We have to pick wisely. And we have to <strong>say no.</strong> Again, again and again.</p>

<p>And the right way to pick, on your own project, is this - and I have learned it well, this time around:</p>

<p><strong>Use what you already know know well.</strong> Think about your end goal. Is it to learn <code class="language-plaintext highlighter-rouge">$tech</code>? Or is it to build an app and get actual users? Do not blend the two. Use something that’s already melted into your fingers, unless you have space for going on adventures.</p>

<p>My biggest mistake was not “not learning Tailwind before”. It was not “not using Next.js”. Tailwind would have crashed just the same under turbopack as it did for me under Rails.</p>

<p>My biggest mistake was trying to combine “learning a new technology” with “delivering an app quickly”. Layout is hard already. Design is hard already. UX is hard already. Hard enough without all of these extra things on top.</p>

<h2 id="but-what-about-tailwind-actually">But what about Tailwind, actually?</h2>

<p>Tailwind is nice, and yet very painful at the same time. The things I really liked:</p>

<ul>
  <li>It offers colocation of HTML and CSS. This is, if anything, its best property.</li>
  <li>It scopes your styles from the get go (and kills enough inheritance for you to mostly ignore inheritance)</li>
  <li>It removes the struggle of “should I use BEM or Emmet or whatever” for naming your CSS classes</li>
  <li>It adds a reasonable set of variables for text sizing and layout spacings</li>
  <li>It gives you some reasonable color tints that have names and are easy to memorize</li>
</ul>

<p>Things I found very painful:</p>

<ul>
  <li>You have to learn another CSS syntax. “Just” CSS is not enough even though modern CSS is already vast. Knowing Tailwind does not free you from having to know standard CSS just as knowing Clojure requires you to know bare Java - and know it well, just as knowing CoffeeScript back in the day required you to know vanilla JS.</li>
  <li>CSS is rife with context - modifiers, <code class="language-plaintext highlighter-rouge">:active</code>, <code class="language-plaintext highlighter-rouge">:first-child</code>, <code class="language-plaintext highlighter-rouge">:not</code> etc. Slapping classes on a particular element sometimes works in this paradigm, and sometimes it does not. Or – does not work that well.</li>
  <li>When you are doing layout - which is hard enough already - you often need to adjust many elements to the same styling, at the same time. Having to do multi-edits directly depends on the capabilities of your editor.</li>
</ul>

<p>Honestly, the two latter ones I could live with - and, after all, I could revert to <code class="language-plaintext highlighter-rouge">@apply</code> - which would drag me back to standard-CSS-but-having-to-express-it-in-Tailwind-for-some-reason. The first one - a dealbreaker.</p>

<p>Why? Well, because doing layout is hard already. I need to do a lot of energy to imagine how my app needs to look, how it should behave. That is already hard enough.</p>

<p>Doing the mental translation from that into CSS, and <em>then</em> another mental translation into Tailwind - is just too much, for little benefit.</p>

<p>What I did find from the Tailwind experience is that I absolutely <em>do</em> want 2 things:</p>

<ul>
  <li>A better CSS reset (primarily - to nullify inheritance)</li>
  <li>A good way to specify localised CSS that does not affect layouts except for the one it is used in</li>
</ul>

<p>For the former I will likely look for a good reset. For the latter - I already found a solution that works beautifully in Rails, does not require any preprocessors or Node modules whatsoever, and completely covers this use case (and more).</p>

<h2 id="thinking-like-a-maintainer">Thinking like a maintainer</h2>

<p>And then there is one more thing. See, I <em>know</em> that shipping end-user software - like the Tailwind compiler - if that software is written in an interpreted language - is fucking hard. I <em>know</em> that getting binaries built for older versions of macOS <a href="https://blog.julik.nl/2014/12/the-hell-of-building-nuke-dylibs">is an absolute abomination.</a> - seriously, just read that article if you don’t believe me. And I absolutely admire Brad, for example, who stubbornly went and <a href="https://terminalwire.com/articles/tebako">built the Terminalwire binaries using tebako.</a></p>

<p>When I worked on <a href="https://github.com/guerilla-di/tracksperanto">Tracksperanto,</a> one of the most popular questions I got from studios was how to install it without having to install Ruby and Rubygems. Like.. shipping scripted products is hard!</p>

<p>But here is the rub: if you play that game, you have to play it well. It is not for nothing the TypeScript compiler is getting moved to Go. It is not for nothing that esbuild, which I consider to be <em>the only</em> web bundler product that was done right, from the start - is written in Go. You can dislike Rob Pike’s attitudes all you want - and I do - but one thing is undeniable: Go is absolutely stellar at the monobinary-for-every-imaginable-platform.</p>

<p>And if you play that game, having both instruction set compatibility and ABI compatibility with the target platforms does, sorta-kinda, become <em>your</em> problem. So, exactly because I <em>do</em> know what I am talking about, if I were in this situation:</p>

<ul>
  <li>I would never pick an experimental runtime for a product I want to be popular</li>
  <li>I would really consider whether parsing attributes in HTML warrants bringing along such a large toolchain (Rust? pnpm? bun? bun-standalone?)</li>
  <li>…and with all those considerations: I would either rewrite Tailwind in Go or leave it be a Node module. Node does work everywhere. Bun does not. Node does have governance which would not allow it to turn into a lemon on a sizeable number of machines, but Bun does not - Bun’s governance is just Jarred.</li>
  <li>If I were shipping a runtime that is multiplatform, I would think thrice before removing support for anything that is not truly critical. And I would know that I am suffering the macOS .framework build toolchain hell for a good reason - for the comfort of <em>end-users,</em> not mine.</li>
</ul>

<p>And yes, I know it is incredibly tempting to just keep developing your software in “that language you know and everybody else in my ecosystem knows”. It can also become a bad bet exactly when you see that this approach has downsides. And yes, it is now your problem too - to be aware of those downsides, and pick the users’ convenience over <em>your</em> comfort as developer.</p>

<ul>
  <li>https://github.com/oven-sh/bun/issues/18885</li>
  <li>https://github.com/oven-sh/bun/issues/7179</li>
  <li>https://github.com/oven-sh/bun/issues/15139</li>
</ul>

<p>And it’s not like there never was a precedent before, in an completely adjacent field too: remember <a href="https://github.com/sass/libsass">libsass?</a> While it did not pursue a monobinary approach - but needed to speed up builds with <em>A Gawddam’ Shitton Of SCSS</em> - and it was C++.</p>

<p>So: yes, I do not find the Tailwind team made the right tradeoffs in picking a platform for Tailwind 4. The desire was noble, but the end result is not worth it. And yes - Tailwind is free, and bashing people on the web is free too - and as per DHH <em>I am not owed anything.</em> But I am entitled to my opinions, and I am well within my right to call out things I consider mistakes. So there.</p>

<p>My personal opinion is that - at least in this case - the adoption of this carrier runtime and this vast toolkit was not done responsibly, and the Tailwind maintainers have made <strong>wrong and inconsiderate choices.</strong> There, I said it.</p>

<h2 id="what-about-the-box-then">What about the box then?</h2>

<p>It’s a mixed bag. <a href="https://www.reddit.com/r/mac/comments/zpe589/strange_usb_issues_with_mac_studio_thunderbolt/">USB audio drops out on reboot.</a> System Settings <a href="https://discussions.apple.com/thread/255870515?sortBy=rank">is a shitshow.</a> Wacom tablet support <a href="https://support.wacom.com/hc/en-us/articles/25915509197335-Does-Wacom-have-a-driver-for-macOS-15-Sequoia">is a drama</a> - and I have to use an older driver version for my Intuos, where the Wacom Center just crashes on startup. The famous <a href="https://x.com/juliknl/status/1905560786799345878">product manager mano-a-mano</a> of <em>Reactions…</em></p>

<p>I can get by. It really is not what I have been dreaming of. And I resent the fact that Tailwind 4 was the thing that made me buy it. It also had to be an older model, because the new ones were on a 4 week backorder in any decent configuration.</p>

<p>Having a CSS pre-processor was not worth that money, and not worth having that resentment.</p>

<h2 id="a-more-important-lesson">A more important lesson</h2>

<p>…for me - is to mute the influencers. There is a list of a handful of folks - they are good folks, but what they are doing is absolutely toxic for what I need to do - namely, build stuff. So I am not only going to go all <code class="language-plaintext highlighter-rouge">@levelsio</code> on it and ignore the “best practice” advice – I will also <em>mute</em> all of them. I don’t want to second-guess myself at every <code class="language-plaintext highlighter-rouge">bundle add</code> and every <code class="language-plaintext highlighter-rouge">brew install</code>. I don’t want to go to sleep with an anxiety that every new tech is going to make me unhireable. And I don’t want to end every day with a feeling that I am a bad developer just because I don’t use nuxt-tailwind-immer-zod-whatever.</p>

<p>Because I know that I can do just fine. If I am allowed space to build.</p>

<p>I know they are not doing this out of malice, but I have to protect my creative spirits at least to some extent.</p>

<p><strong>Update:</strong> Well, it also seems it wasn’t the end of it <a href="https://github.com/tailwindlabs/tailwindcss/issues/17728#issuecomment-2884340873">as this GH issue shows</a></p>]]></content><author><name></name></author><summary type="html"><![CDATA[The day was nearing to a close. The sun has already set, but that Friday evening in Amsterdam was still warm. Unusually warm, in fact, for those late days in March – as if spring decided to bless my pilgrimage, for that pilgrimage was not jovial. I was sitting at a ramen joint, sipping on the broth. To my left, a blue, crinkled supermarket shopping bag was sitting solemnly, inconspicuously. Inside that bag sat a slightly used Mac Studio, which I have just purchased to be able to edit CSS of my own application. By the time that evening descended upon the south of Amsterdam, I have lost 3 days of my life trying to figure out why I was unable to edit CSS. But let me rewind a bit.]]></summary></entry><entry><title type="html">UI algorithms: a tiny undo stack</title><link href="https://blog.julik.nl/2025/03/a-tiny-undo-stack" rel="alternate" type="text/html" title="UI algorithms: a tiny undo stack" /><published>2025-03-22T00:00:00+00:00</published><updated>2025-03-22T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/03/a-tiny-undo-stack</id><content type="html" xml:base="https://blog.julik.nl/2025/03/a-tiny-undo-stack"><![CDATA[<p>I’ve needed this before - a couple of times. Third time I figured I needed something small, nimble - yet complete. And - at the same time - wondering about how to do it in a very simple manner. I think it worked out great, so let’s dig in.</p>

<p>Most UIs will have some form of undo functionality. Now, there are generally two forms of it: <em>undo stacks</em> and <em>version histories</em>. A “version history” is what Photoshop history gives you - the ability to “paint through” to a previous state of the system. You can add five paint strokes, and then reveal a stroke you have made 4 steps back.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>But most apps won’t need that. What you will need is an <em>undo stack,</em> which can be specced out as follows:</p>

<ul>
  <li>An undoable action gets performed and gets pushed onto the stack.</li>
  <li>If undo is requested, the stack is popped and the rollback action gets applied for the popped action.</li>
  <li>If an action was undone, you can redo that action. If you have undone 2 actions, you can redo 2 actions.</li>
  <li>If you push an undoable action onto the stack in presence of actions that can be redone, they get discarded - there is no branching, remember?</li>
</ul>

<p>If you are curious how “the big guys” used to do it - check out the <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/UndoArchitecture/Articles/RegisteringUndo.html#//apple_ref/doc/uid/20000206-SW2">NSUndoManager documentation</a></p>

<p>So, as I usually like to do, I want to understand the API that would be optimal. For this use case - drawing - I had the following workflow:</p>

<ul>
  <li>When you draw a stroke the input points get added to <code class="language-plaintext highlighter-rouge">currentStroke</code></li>
  <li>When you release the pen the <code class="language-plaintext highlighter-rouge">currentStroke</code> gets appended to <code class="language-plaintext highlighter-rouge">strokes</code> and reset for the next stroke.</li>
</ul>

<p>I wanted something like this:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">addStroke</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">strokes</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">currentPaintStroke</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">removeStroke</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">strokes</span><span class="p">.</span><span class="nf">pop</span><span class="p">();</span>
<span class="nx">undoThing</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">addStroke</span><span class="p">,</span> <span class="nx">removeStroke</span><span class="p">);</span>

<span class="c1">// then, on user action</span>
<span class="nx">undoThing</span><span class="p">.</span><span class="nf">undo</span><span class="p">();</span> <span class="c1">// calls removeStroke()</span>
<span class="nx">undoThing</span><span class="p">.</span><span class="nf">redo</span><span class="p">();</span> <span class="c1">// calls strokes.push(...) again</span>
</code></pre></div></div>

<h2 id="the-perils-of-stack-pointers">The perils of stack pointers</h2>

<p>Simplest thing in the world. Now, if you look at most recommended (and some existing!) implementations of an undo stack, you will find they usually make use of a stack with a pointer. Like <a href="https://www.npmjs.com/package/undo-manager?activeTab=code">here</a> and <a href="https://medium.com/fbbd/intro-to-writing-undo-redo-systems-in-javascript-af17148a852b">here</a> - you would have a stack, usually represented as a JS array, and some kind of <code class="language-plaintext highlighter-rouge">pointer</code> or an <code class="language-plaintext highlighter-rouge">index</code> that you would use to index into it.</p>

<p>And while it is workable <em>and</em> standard, it just didn’t jive with me well. See, using an index into an array usually makes JS code susceptible to two things, which bite me every single time:</p>

<ul>
  <li>Indexing into a nonexistent index - hello <code class="language-plaintext highlighter-rouge">undefined</code> checks</li>
  <li>Mistakes in offsets when calling <code class="language-plaintext highlighter-rouge">Array.slice</code> and <code class="language-plaintext highlighter-rouge">Array.splice</code>. Oh, and confusing <code class="language-plaintext highlighter-rouge">slice</code> and <code class="language-plaintext highlighter-rouge">splice</code>, of course.</li>
</ul>

<p>The fact that Ruby and JS have different semantics for <code class="language-plaintext highlighter-rouge">slice</code> - one uses the index bounds, the other uses two offsets - doesn’t help things. And what happens if an API uses offsets into a vector? Exactly: confusion whether those offsets are inclusive or exclusive. Oh, and the offsets change after you mutate the array, which makes it even more painful.</p>

<h2 id="could-we-not-index">Could we not index?</h2>

<p>So what came to mind was this: we effectively have two stacks, not one. We have an <code class="language-plaintext highlighter-rouge">undoStack</code> (things that can be rolled back) and a <code class="language-plaintext highlighter-rouge">redoStack</code> - things that can be rolled forward. All the things we do with our undo-redo actions actually do not change the <em>pointer</em> - they <em>move</em> things from one stack to another. And rules change between these two stacks! We erase the redoable actions when we add a new undoable action, remember? So while an undoable stack will rarely get “nullified”, the redoable stack likely will be nullified frequently.</p>

<p>Once this became clear, the implementation practically wrote itself:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">createUndoStack</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="nx">past</span> <span class="o">=</span> <span class="p">[];</span>
  <span class="kd">let</span> <span class="nx">future</span> <span class="o">=</span> <span class="p">[];</span>

  <span class="k">return</span> <span class="p">{</span>
    <span class="nf">push</span><span class="p">(</span><span class="nx">doFn</span><span class="p">,</span> <span class="nx">undoFn</span><span class="p">)</span> <span class="p">{</span>
      <span class="nf">doFn</span><span class="p">();</span>
      <span class="nx">past</span><span class="p">.</span><span class="nf">push</span><span class="p">({</span><span class="nx">doFn</span><span class="p">,</span> <span class="nx">undoFn</span><span class="p">});</span>
      <span class="c1">// Adding a new action wipes the redoable steps</span>
      <span class="nx">future</span><span class="p">.</span><span class="nx">length</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="p">},</span>
    <span class="nf">undo</span><span class="p">()</span> <span class="p">{</span>
     <span class="kd">let</span> <span class="nx">action</span> <span class="o">=</span> <span class="nx">past</span><span class="p">.</span><span class="nf">pop</span><span class="p">();</span>
     <span class="k">if </span><span class="p">(</span><span class="nx">action</span><span class="p">)</span> <span class="p">{</span>
       <span class="nx">action</span><span class="p">.</span><span class="nf">undoFn</span><span class="p">();</span>
       <span class="nx">future</span><span class="p">.</span><span class="nf">unshift</span><span class="p">(</span><span class="nx">action</span><span class="p">);</span>
     <span class="p">}</span>
    <span class="p">},</span>
    <span class="nf">redo</span><span class="p">()</span> <span class="p">{</span>
      <span class="kd">let</span> <span class="nx">action</span> <span class="o">=</span> <span class="nx">future</span><span class="p">.</span><span class="nf">shift</span><span class="p">();</span>
      <span class="k">if </span><span class="p">(</span><span class="nx">action</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">action</span><span class="p">.</span><span class="nf">doFn</span><span class="p">();</span>
        <span class="nx">past</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">action</span><span class="p">);</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>So instead of trying to save resources by having just one array (and miserably failing with off-by-one index errors), we can embrace dynamically sized arrays and just forget indices altogether. Neat!</p>

<p>Let’s add a couple more methods to display our UI:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">get</span> <span class="nf">canUndo</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">past</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">;</span>
  <span class="p">},</span>
  <span class="kd">get</span> <span class="nf">canRedo</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">future</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">;</span>
  <span class="p">}</span>
</code></pre></div></div>

<h2 id="the-call-by-sharing-problem">The call-by-sharing problem</h2>

<p>There is a catch with our implementation though. JS has rather interesting lexical scoping rules: what is defined in the scope of the definition of the function will be referenced from within the function. This means that when we start pulling a new <code class="language-plaintext highlighter-rouge">currentStroke</code> our <code class="language-plaintext highlighter-rouge">undoFn</code> closure will not use a copy of the <code class="language-plaintext highlighter-rouge">currentStroke</code> it was created with, but our current one. And our <code class="language-plaintext highlighter-rouge">doFn</code> and <code class="language-plaintext highlighter-rouge">undoFn</code> must satisfy an important guarantee: they must be idempotent. No matter what the state of the surrounding system is, appending the <code class="language-plaintext highlighter-rouge">currentStroke</code> should always append the stroke the <code class="language-plaintext highlighter-rouge">redoFn</code> was created for.</p>

<p>If we do not take care of this, the following <code class="language-plaintext highlighter-rouge">doFn</code>:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">doFn</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">strokes</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">currentStroke</span><span class="p">)</span>
</code></pre></div></div>

<p>is going to grab the <code class="language-plaintext highlighter-rouge">currentStroke</code> from the surrounding scope (whatever its value is) and append it to the <code class="language-plaintext highlighter-rouge">strokes</code> array. The <code class="language-plaintext highlighter-rouge">currentStroke</code> at that time may be just empty. To avoid this behavior, we want our <code class="language-plaintext highlighter-rouge">doFn</code> to use a cloned copy of the <code class="language-plaintext highlighter-rouge">currentStroke</code> - current at time of definition of <code class="language-plaintext highlighter-rouge">doFn</code>, and we want it to do so always. If your undoable action is some kind of <code class="language-plaintext highlighter-rouge">delete</code> (“pop”) you want the reverse for your <code class="language-plaintext highlighter-rouge">undoFn</code> - the undo function must push the deleted object back into the array, and not mutate it in any way. To create a deep copy of our <code class="language-plaintext highlighter-rouge">currentStroke</code>, modern JS offers us a feature called <code class="language-plaintext highlighter-rouge">structuredClone()</code>. We can use the <code class="language-plaintext highlighter-rouge">...</code> rest parameters to package any arguments into one array, which we will then clone:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nf">push</span><span class="p">(</span><span class="nx">doFn</span><span class="p">,</span> <span class="nx">undoFn</span><span class="p">,</span> <span class="p">...</span><span class="nx">withArgumentsToClone</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">clonedArgs</span> <span class="o">=</span> <span class="nf">structuredClone</span><span class="p">(</span><span class="nx">withArgumentsToClone</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">action</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nf">doWithData</span><span class="p">()</span> <span class="p">{</span>
        <span class="nf">doFn</span><span class="p">(...</span><span class="nx">clonedArgs</span><span class="p">);</span>
      <span class="p">},</span>
      <span class="nf">undoWithData</span><span class="p">()</span> <span class="p">{</span>
        <span class="nf">undoFn</span><span class="p">(...</span><span class="nx">clonedArgs</span><span class="p">);</span>
      <span class="p">},</span>
    <span class="p">};</span>
    <span class="nx">action</span><span class="p">.</span><span class="nf">doWithData</span><span class="p">();</span>
  
    <span class="c1">// Adding a new action wipes the redoable steps</span>
    <span class="nx">past</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">action</span><span class="p">);</span>
    <span class="nx">future</span><span class="p">.</span><span class="nx">length</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>and we’ll amend our functions accordingly. Instead of closuring over <code class="language-plaintext highlighter-rouge">currentStroke</code> we’ll make it an argument:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>let appendStroke = strokes.push.bind(strokes);
undoStack.push(appendStroke, () =&gt; strokes.pop(), currentStroke);
</code></pre></div></div>

<p>with the <code class="language-plaintext highlighter-rouge">push()</code> of our <code class="language-plaintext highlighter-rouge">undoStack</code> taking care of making a deep clone for us. Nice!</p>

<p>The complete definition then becomes:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">createUndoStack</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">past</span> <span class="o">=</span> <span class="p">[];</span>
  <span class="kd">const</span> <span class="nx">future</span> <span class="o">=</span> <span class="p">[];</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="nf">push</span><span class="p">(</span><span class="nx">doFn</span><span class="p">,</span> <span class="nx">undoFn</span><span class="p">,</span> <span class="p">...</span><span class="nx">withArgumentsToClone</span><span class="p">)</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">clonedArgs</span> <span class="o">=</span> <span class="nf">structuredClone</span><span class="p">(</span><span class="nx">withArgumentsToClone</span><span class="p">);</span>
      <span class="kd">const</span> <span class="nx">action</span> <span class="o">=</span> <span class="p">{</span>
        <span class="nf">doWithData</span><span class="p">()</span> <span class="p">{</span>
          <span class="nf">doFn</span><span class="p">(...</span><span class="nx">clonedArgs</span><span class="p">);</span>
        <span class="p">},</span>
        <span class="nf">undoWithData</span><span class="p">()</span> <span class="p">{</span>
          <span class="nf">undoFn</span><span class="p">(...</span><span class="nx">clonedArgs</span><span class="p">);</span>
        <span class="p">},</span>
      <span class="p">};</span>
      <span class="nx">action</span><span class="p">.</span><span class="nf">doWithData</span><span class="p">();</span>

      <span class="c1">// Adding a new action wipes the redoable steps</span>
      <span class="nx">past</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">action</span><span class="p">);</span>
      <span class="nx">future</span><span class="p">.</span><span class="nx">length</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="p">},</span>
    <span class="nf">undo</span><span class="p">()</span> <span class="p">{</span>
      <span class="kd">let</span> <span class="nx">action</span> <span class="o">=</span> <span class="nx">past</span><span class="p">.</span><span class="nf">pop</span><span class="p">();</span>
      <span class="k">if </span><span class="p">(</span><span class="nx">action</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">action</span><span class="p">.</span><span class="nf">undoWithData</span><span class="p">();</span>
        <span class="nx">future</span><span class="p">.</span><span class="nf">unshift</span><span class="p">(</span><span class="nx">action</span><span class="p">);</span>
      <span class="p">}</span>
    <span class="p">},</span>
    <span class="nf">redo</span><span class="p">()</span> <span class="p">{</span>
      <span class="kd">let</span> <span class="nx">action</span> <span class="o">=</span> <span class="nx">future</span><span class="p">.</span><span class="nf">shift</span><span class="p">();</span>
      <span class="k">if </span><span class="p">(</span><span class="nx">action</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">action</span><span class="p">.</span><span class="nf">doWithData</span><span class="p">();</span>
        <span class="nx">past</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">action</span><span class="p">);</span>
      <span class="p">}</span>
    <span class="p">},</span>
    <span class="kd">get</span> <span class="nf">undoAvailable</span><span class="p">()</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">past</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">;</span>
    <span class="p">},</span>
    <span class="kd">get</span> <span class="nf">redoAvailable</span><span class="p">()</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">future</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">;</span>
    <span class="p">},</span>
    <span class="nf">clear</span><span class="p">()</span> <span class="p">{</span>
      <span class="nx">past</span><span class="p">.</span><span class="nx">length</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
      <span class="nx">future</span><span class="p">.</span><span class="nx">length</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
      <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">export</span> <span class="p">{</span><span class="nx">createUndoStack</span><span class="p">};</span>
</code></pre></div></div>

<p>Robust, small, and no indexing errors. My jam.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I’ve needed this before - a couple of times. Third time I figured I needed something small, nimble - yet complete. And - at the same time - wondering about how to do it in a very simple manner. I think it worked out great, so let’s dig in. Most UIs will have some form of undo functionality. Now, there are generally two forms of it: undo stacks and version histories. A “version history” is what Photoshop history gives you - the ability to “paint through” to a previous state of the system. You can add five paint strokes, and then reveal a stroke you have made 4 steps back.]]></summary></entry><entry><title type="html">Musings on module registration (and why it could be better in Rails)</title><link href="https://blog.julik.nl/2025/02/on-module-registration" rel="alternate" type="text/html" title="Musings on module registration (and why it could be better in Rails)" /><published>2025-02-17T00:00:00+00:00</published><updated>2025-02-17T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/02/on-module-registration</id><content type="html" xml:base="https://blog.julik.nl/2025/02/on-module-registration"><![CDATA[<p>Having the same architecture problems over and over does give you perspective. We all love making fun of the enterprise FizzBuzz but there are cases where those Factories, Adapters and Facades are genuinely very useful, and so is dependency injection. Since I had to do dependency injection combined with adapters a wee many times now, it seems like a good idea to share my experience.</p>

<p>What I will describe here mostly applies to Ruby, but it mostly applies to the other languages and runtimes too.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="a-refresher-on-adapters">A refresher on adapters</h2>

<p>An adapter is a well known pattern where an object acts as a proxy between the caller and the callee. Since the whole FP-OOP fight is utter nonsense, functions are great adapters too. Observe:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">DatabaseAdapterSQLite</span>
  <span class="k">def</span> <span class="nf">get_first_value</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
    <span class="vi">@db</span><span class="p">.</span><span class="nf">get_first_value</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">DatabaseAdapterActiveRecord</span>
  <span class="k">def</span> <span class="nf">get_first_value</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
    <span class="vi">@connection</span><span class="p">.</span><span class="nf">select_value</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>But in, say, JS:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">selectOneSQLite</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">sqliteConnection</span><span class="p">.</span><span class="nf">getFirstValue</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nf">selectOneMySQL</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">mysqlConn</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="nx">query</span> <span class="o">+</span> <span class="dl">"</span><span class="s2"> LIMIT 1</span><span class="dl">"</span><span class="p">).</span><span class="nf">firstRow</span><span class="p">().</span><span class="nf">firstColumn</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>An adapter is nothing more than a module. Whether it is a function, a closure or an object is irrelevant. The important part is that we have multiple modules that implement the same API and allow calling them without having to know which module our call gets dispatched to (late binding).</p>

<h2 id="module-table-shape">Module table shape</h2>

<p>There are generally two patterns I’ve seen for using multiple modules with a single piece of input (a dispatch of a function call / method call without arguments can also count as input):</p>

<ul>
  <li>Modules need to be applied in some order, until a module provides output that is the best for the job.</li>
  <li>There is some kind of table where the module to be called gets looked up.</li>
</ul>

<p>There are also combinations of the two. For example, in <a href="https://github.com/WeTransfer/format_parser">format_parser</a> it is mostly the former and sometimes the latter. The main table is an array of modules ordered by priority. More specific parsers - for files that are less likely to occur - are at the start of that list. The reason for that is that multiple files may detect as TIFFs or JPEGs, and parsing for some file types is less reliable / less confident, depending on the file format. The parsing then goes in this manner, roughly:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">parsers_ordered_by_priority</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">parser</span><span class="o">|</span>
  <span class="n">maybe_result</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">file</span><span class="p">)</span>
  <span class="k">return</span> <span class="n">maybe_result</span> <span class="k">if</span> <span class="n">maybe_result</span>
<span class="k">end</span>
</code></pre></div></div>

<p>However, there is also a couple of tables for doing the latter approach. If you ask, for example, to only have <code class="language-plaintext highlighter-rouge">image</code>-natured files parsed - omitting a file it is not an image - there are also auxiliary tables per nature:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">parsers_for_nature</span><span class="p">(</span><span class="ss">:image</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">parser</span><span class="o">|</span>
  <span class="c1"># In this case `parsers_for_nature(:image)` is already</span>
  <span class="c1"># sorted by the same priority as `parsers_ordered_by_priority`,</span>
  <span class="c1"># so less common filetypes will be checked first</span>
  <span class="n">maybe_result</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">file</span><span class="p">)</span>
  <span class="k">return</span> <span class="n">maybe_result</span> <span class="k">if</span> <span class="n">maybe_result</span>
<span class="k">end</span>
</code></pre></div></div>

<p>There is also a table of known file types that you may want to detect for:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">parsers_for_format</span><span class="p">(</span><span class="ss">:cr2</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">parser</span><span class="o">|</span>
  <span class="c1"># In this case `parsers_for_format` is already</span>
  <span class="c1"># sorted by the same priority as `parsers_ordered_by_priority`,</span>
  <span class="c1"># so less common filetypes will be checked first</span>
  <span class="n">maybe_result</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">file</span><span class="p">)</span>
  <span class="k">return</span> <span class="n">maybe_result</span> <span class="k">if</span> <span class="n">maybe_result</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This is probably the biggest example of module registration I have done in my career, with more than a dozen different parsers being plugged into it. The actual code is slightly more involved, you can examine it <a href="https://github.com/WeTransfer/format_parser/blob/master/lib/format_parser.rb#L260">here</a> but the functionality is that. At <a href="https://cheddar.me">Cheddar</a> we have such a table for talking to different banks. That one is lookup-only:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bank_connector</span> <span class="o">=</span> <span class="n">lookup_connector_for</span><span class="p">(</span><span class="s2">"barclays"</span><span class="p">)</span>
<span class="n">accounts</span> <span class="o">=</span> <span class="n">bank_connector</span><span class="p">.</span><span class="nf">query_accounts_for</span><span class="p">(</span><span class="ss">account_access_consent_id: </span><span class="n">aac_id</span><span class="p">)</span>
</code></pre></div></div>

<p>So let’s go and make a module table!</p>

<h2 id="creating-a-module-table">Creating a module table</h2>

<p>There are a few parts to doing this well. First, it has to be <strong>convenient to register a module</strong> with such a table. Second, it should be <strong>cheap to do a lookup in that table.</strong> Let’s omit the case where we need to do detection by applying modules in sequence, and focus on a table use case first. Let’s imagine we have a <code class="language-plaintext highlighter-rouge">UserDetailsProvider</code> interface, and we want to create a module table for different providers of user data. The API we will need to support is going to be just one method - <code class="language-plaintext highlighter-rouge">call(email)</code>. So the module we register needs to be callable, and should return user data from that provider.</p>

<p>Nothing is easier:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">google_details_provider</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">email</span><span class="p">)</span> <span class="p">{</span>
  <span class="no">GoogleAPI</span><span class="o">::</span><span class="no">Workspace</span><span class="p">.</span><span class="nf">details_for_account</span><span class="p">(</span><span class="n">email</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">facebook_details_provider</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">email</span><span class="p">)</span> <span class="p">{</span>
  <span class="no">Facebook</span><span class="o">::</span><span class="no">Users</span><span class="p">.</span><span class="nf">lookup</span><span class="p">(</span><span class="ss">email: </span><span class="n">email</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">internal_provider</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">email</span><span class="p">)</span> <span class="p">{</span> <span class="no">User</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">email: </span><span class="n">email</span><span class="p">).</span><span class="nf">first</span> <span class="p">}</span>

<span class="n">module_table</span> <span class="o">=</span> <span class="p">{</span>
  <span class="s2">"google"</span> <span class="o">=&gt;</span> <span class="n">google_details_provider</span><span class="p">,</span>
  <span class="s2">"facebook"</span> <span class="o">=&gt;</span> <span class="n">facebook_details_provider</span><span class="p">,</span>
  <span class="s2">"internal"</span> <span class="o">=&gt;</span> <span class="n">internal_provider</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>

<p>When we need to lookup a module, we do a simple <code class="language-plaintext highlighter-rouge">Hash#fetch</code>. The use of <code class="language-plaintext highlighter-rouge">#fetch</code> allows us to either raise a <code class="language-plaintext highlighter-rouge">KeyError</code> if we do not have a provider:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">module_table</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="n">provider_name</span><span class="p">).</span><span class="nf">call</span><span class="p">(</span><span class="n">email</span><span class="p">)</span>
</code></pre></div></div>

<p>or use our “internal” provider as default:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">module_table</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="n">provider_name</span><span class="p">,</span> <span class="n">internal_provider</span><span class="p">).</span><span class="nf">call</span><span class="p">(</span><span class="n">email</span><span class="p">)</span>
</code></pre></div></div>

<p>That’s it to satisfy the <strong>cheap to do a lookup</strong> requirement. Following from that are conveniences.</p>

<h2 id="creating-a-convenient-module-table">Creating a convenient module table</h2>

<p>Conveniences are subjective, of course. There are entire <a href="https://go.dev/">programming language ecosystems</a> where not providing conveniences is touted as a virtue - so it depends on the style preference, in large part. However, I do subscribe to the notion that working on a system we <strong>must</strong> make reasonable effort to make it convenient - both to end users and for people who are going to work on the system in the future. With module registration, I set the following rules defining convenience:</p>

<ul>
  <li>Adding a module should be possible from a namespace external to the namespace in which lookup gets done</li>
  <li>Removing a module from the table should be possible – primarily for testing</li>
  <li>Removing a file that adds its modules into the table should also remove the module registration.</li>
</ul>

<p>Illustrating the first point:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">google_details_provider</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">email</span><span class="p">)</span> <span class="p">{</span>
  <span class="no">GoogleAPI</span><span class="o">::</span><span class="no">Workspace</span><span class="p">.</span><span class="nf">details_for_account</span><span class="p">(</span><span class="n">email</span><span class="p">)</span>
<span class="p">}</span>
<span class="no">UserDetails</span><span class="p">.</span><span class="nf">register_provider</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"google"</span><span class="p">,</span> <span class="ss">provider: </span><span class="n">google_details_provider</span><span class="p">)</span>
</code></pre></div></div>

<p>The second point - and how it would be used:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">test_custom_provider</span>
  <span class="n">custom_provider</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">email</span><span class="p">)</span> <span class="p">{</span> <span class="ss">:VIP</span> <span class="p">}</span>
  <span class="no">UserDetails</span><span class="p">.</span><span class="nf">register_provider</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"test"</span><span class="p">,</span> <span class="ss">provider: </span><span class="n">custom_provider</span><span class="p">)</span>

  <span class="n">test_user</span><span class="p">.</span><span class="nf">user_details_provider_name</span> <span class="o">=</span> <span class="s2">"test"</span>
  <span class="n">assert_equal</span> <span class="ss">:VIP</span><span class="p">,</span> <span class="n">test_user</span><span class="p">.</span><span class="nf">get_user_details</span>
<span class="k">ensure</span>
  <span class="no">UserDetails</span><span class="p">.</span><span class="nf">deregister_provider</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"test"</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And the third. It’s way simpler than it seems - we just place the <code class="language-plaintext highlighter-rouge">google_details_provider</code> inside a separate file, and make sure this file does get <code class="language-plaintext highlighter-rouge">require</code>d somewhere in the caller code. Note that this <strong>will not happen by default in Rails</strong> - which is an important consideration:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># google_details_provider.rb</span>
<span class="n">google_details_provider</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">email</span><span class="p">)</span> <span class="p">{</span> <span class="no">GoogleAPI</span><span class="o">::</span><span class="no">Workspace</span><span class="p">.</span><span class="nf">details_for_account</span><span class="p">(</span><span class="n">email</span><span class="p">)</span> <span class="p">}</span>
<span class="no">UserDetails</span><span class="p">.</span><span class="nf">register_provider</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"google"</span><span class="p">,</span> <span class="ss">provider: </span><span class="n">google_details_provider</span><span class="p">)</span>
</code></pre></div></div>

<p>Once we remove <code class="language-plaintext highlighter-rouge">google_details_provider.rb</code> from our source tree, its registration will also be removed - nothing to do. More <a href="https://htmx.org/essays/locality-of-behaviour/">locality of behavior</a>
and less things to manage overall.</p>

<p>I normally also create a method which returns all the registered modules, so that they can be subjected to conformance testing:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">UserDetails</span><span class="p">.</span><span class="nf">with_each_known_provider</span> <span class="k">do</span> <span class="o">|</span><span class="nb">name</span><span class="p">,</span> <span class="n">provider</span><span class="o">|</span>
  <span class="n">assert_conformant_provider</span><span class="p">(</span><span class="n">provider</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The registration from the module file I tend to do like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">GoogleProvider</span> <span class="c1"># FacebookProvider, AppleProvider...</span>
  <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">email_address</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="c1"># Make the module itself callable - no need to create instances</span>
  <span class="kp">extend</span> <span class="nb">self</span>

  <span class="c1"># Register the module just as it gets defined</span>
  <span class="no">UserDetails</span><span class="p">.</span><span class="nf">register_provider</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"google"</span><span class="p">,</span> <span class="ss">provider: </span><span class="nb">self</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Note that <code class="language-plaintext highlighter-rouge">GoogleProvider</code> does need to be anywhere inside <code class="language-plaintext highlighter-rouge">UserDetails</code> namespace-wise. Neither is there any smart resolving done.</p>

<h2 id="playing-nice-with-rails-autoloading">Playing nice with Rails autoloading</h2>

<p>When I use this pattern, I tend to have my modules eagerly loaded - because in Rails a provider is only going to be <code class="language-plaintext highlighter-rouge">require</code>d by Zeitwerk once you try to use its constant in the code:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">GoogleProvider</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">email</span><span class="p">)</span> <span class="c1"># Will attempt to load google_provider.rb</span>
</code></pre></div></div>

<p>To make module registration work, you need to register your modules from files which get loaded eagerly - like an initializer, and you need to register the module <em>names</em> as opposed to callable objects themselves:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">module_table</span> <span class="o">=</span> <span class="p">{</span>
  <span class="s2">"google"</span> <span class="o">=&gt;</span> <span class="s2">"GoogleDetailsProvider"</span><span class="p">,</span>
  <span class="s2">"facebook"</span> <span class="o">=&gt;</span> <span class="s2">"FacebookDetailsProvider"</span><span class="p">,</span>
<span class="p">}</span>

<span class="c1"># Calling "String#constantize" forces Zeitwerk to attempt autoloading of the module</span>
<span class="n">module_table</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="s2">"InternalProvider"</span><span class="p">).</span><span class="nf">constantize</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">email</span><span class="p">)</span>
</code></pre></div></div>

<p>It is a passable compromise, but for it to work adding the module names to the table must be done outside of the modules themselves. Consequently, if you remove a file defining a module, its registration will still be performed - but the call into the module will fail at call site (as the constant will not be resolved). The way I prefer to fix this is to require modules eagerly. This can be done using <code class="language-plaintext highlighter-rouge">Dir.glob</code>. Note that I <code class="language-plaintext highlighter-rouge">sort</code> the glob output because the order in which glob results get returned is OS-dependent.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Dir</span><span class="p">.</span><span class="nf">glob</span><span class="p">(</span><span class="no">File</span><span class="p">.</span><span class="nf">dirname</span><span class="p">(</span><span class="kp">__FILE__</span><span class="p">)</span> <span class="o">+</span> <span class="s2">"/user_information_providers/*_provider.rb"</span><span class="p">).</span><span class="nf">sort</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">path</span><span class="o">|</span>
  <span class="nb">require</span> <span class="n">path</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The modules get defined, and the self-registration code runs immediately. In Rails you must do this inside of <code class="language-plaintext highlighter-rouge">ActiveSupport::Reloader.to_prepare</code> block, so that what you <code class="language-plaintext highlighter-rouge">require</code> will be subjected to the same live-reloading Zeitwerk enables for most other modules inside your Rails app:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Reloader</span><span class="p">.</span><span class="nf">to_prepare</span> <span class="k">do</span>
  <span class="no">Dir</span><span class="p">.</span><span class="nf">glob</span><span class="p">(</span><span class="no">Rails</span><span class="p">.</span><span class="nf">root</span> <span class="o">+</span> <span class="s2">"/app/user_information_providers/*_provider.rb"</span><span class="p">).</span><span class="nf">sort</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">path</span><span class="o">|</span>
    <span class="nb">require</span> <span class="n">path</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Since we would be using instantiation here, we would self-register from a class:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">GoogleProvider</span>
  <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">email_address</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="c1"># Register the module just as it gets defined</span>
  <span class="no">UserDetails</span><span class="p">.</span><span class="nf">register_provider</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"google"</span><span class="p">,</span> <span class="ss">provider: </span><span class="nb">to_s</span><span class="p">)</span> <span class="c1"># this calls Class#to_s</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="sad-state-of-rails-module-registration">Sad state of Rails module registration</h2>

<p>In my work I have to implement multiple extensions to Rails, with those extensions being not hacks - but official plug-in features, designed to be used in specific, documented “cut points” for external integrations. So far, under my belt:</p>

<ul>
  <li>2 ActiveJob adapters</li>
  <li>1 cache store adapter</li>
  <li>2 ActiveStorage services</li>
</ul>

<p>Now, let’s rehash the requirements I set:</p>

<ul>
  <li>It should be convenient to register a module</li>
  <li>Module lookup should be cheap</li>
  <li>Adding a module should be possible from a namespace external to the namespace in which lookup gets done</li>
  <li>Removing a module from the table should be possible – primarily for testing</li>
  <li>Removing a file that adds its modules into the table should also remove the module registration.</li>
</ul>

<p>Sadly, the way Rails does module registration is lacking on all 5. The approach differs between Rails components - presumably, due to different people having built them in the first place, but what does remain a recurring theme is the fact that module registration involves the following two things:</p>

<ul>
  <li>Defining modules inside of Rails namespaces</li>
  <li>Having to define them in files that satisfy the Rails file naming conventions - including the Rails default namespacing.</li>
</ul>

<p>This creates extra files that serve very little purpose, and forces one to pollute the namespaces that are not theirs - which is completely unnecessary. I never had to do an ActiveRecord connection adapter, but there the situation is the same. Just look at <a href="https://github.com/trilogy-libraries/activerecord-trilogy-adapter/blob/main/lib/active_record/connection_adapters/trilogy_adapter.rb">trilogy shim</a> for a good example of that. To give a bit more detail, let’s examine the component I am currently working with - ActiveStorage (expect a nice announcement in that area soon). The entry point for lookup in a Rails app is a configuration file called <code class="language-plaintext highlighter-rouge">storage.yml</code>, which defines your storage service as follows:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">main</span><span class="pi">:</span>
  <span class="na">public</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="na">service</span><span class="pi">:</span> <span class="s">Disk</span>
  <span class="na">root</span><span class="pi">:</span> <span class="s">&lt;%= Rails.root.join("tmp/storage") %&gt;</span>
</code></pre></div></div>

<p>Imagine that you want to implement a file storage solution using the blockchain (I won’t judge, let’s pretend):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">main</span><span class="pi">:</span>
  <span class="na">public</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="na">service</span><span class="pi">:</span> <span class="s">Blockchain</span>
  <span class="na">ledger_path_</span><span class="pi">:</span> <span class="s">&lt;%= Rails.root.join("ledgers/file_storage.blockchain") %&gt;</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">service</code> key is what is responsible for module lookup. An <code class="language-plaintext highlighter-rouge">ActiveStorage::Service</code> class is the one that gets looked up, and then instantiated - passing it the options like <code class="language-plaintext highlighter-rouge">public: true</code> and <code class="language-plaintext highlighter-rouge">root: some_path</code>. The interesting bit is how Rails resolves the string <code class="language-plaintext highlighter-rouge">"Disk"</code> to a concrete class to instantiate. Same would need to be done for your <code class="language-plaintext highlighter-rouge">Blockchain</code> service, even though it has nothing to do with Rails internals - it likely <em>uses</em> them, but <em>is not a part of them.</em> Let’s look at the pertinent part of the Rails source, from <code class="language-plaintext highlighter-rouge">active_storage/service/configurator.rb</code> (here for version 7.2.2):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">resolve</span><span class="p">(</span><span class="n">class_name</span><span class="p">)</span>
  <span class="nb">require</span> <span class="s2">"active_storage/service/</span><span class="si">#{</span><span class="n">class_name</span><span class="p">.</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">underscore</span><span class="si">}</span><span class="s2">_service"</span>
  <span class="no">ActiveStorage</span><span class="o">::</span><span class="no">Service</span><span class="p">.</span><span class="nf">const_get</span><span class="p">(</span><span class="ss">:"</span><span class="si">#{</span><span class="n">class_name</span><span class="p">.</span><span class="nf">camelize</span><span class="si">}</span><span class="ss">Service"</span><span class="p">)</span>
<span class="k">rescue</span> <span class="no">LoadError</span>
  <span class="k">raise</span> <span class="s2">"Missing service adapter for </span><span class="si">#{</span><span class="n">class_name</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>So:</p>

<ul>
  <li>Rails assumes that there will be a file called <code class="language-plaintext highlighter-rouge">active_storage/service/blockchain_service.rb</code> somewhere on the <code class="language-plaintext highlighter-rouge">LOAD_PATH</code> and requires it. Adding files to <code class="language-plaintext highlighter-rouge">LOAD_PATH</code> slows down the entire application, <a href="https://bugs.ruby-lang.org/issues/21032">sometimes in interesting ways</a>. Moreover - every gem you require, should it be not very careful, adds to <code class="language-plaintext highlighter-rouge">$LOAD_PATH</code> - and every other piece of code doing a <code class="language-plaintext highlighter-rouge">require</code> will have its own <code class="language-plaintext highlighter-rouge">require</code> run <em>slower</em> as a result.</li>
  <li>Rails assumes that the class name for your service is <code class="language-plaintext highlighter-rouge">BlockchainService</code>, and that this class is inside the <code class="language-plaintext highlighter-rouge">ActiveStorage::Service</code> namespace.</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">$LOAD_PATH</code> lookups are not a joke! In my current application, there are 318 items in the load path, and lookup of a new file there can’t be done quicker than linear. It can also involve file stat checks.</p>

<p>And from the standpoint of code organization: this means that you can’t really provide a service without invading the Rails namespace. What if Rails starts shipping their own <code class="language-plaintext highlighter-rouge">BlockchainService</code>? Whoops. What if you don’t want to add things to LOAD_PATH? Whoops. Is it possible to register your <code class="language-plaintext highlighter-rouge">BlockchainService</code> from an external file (say, a <code class="language-plaintext highlighter-rouge">Railtie</code>) - without defining a “shim” file that will be <code class="language-plaintext highlighter-rouge">require</code>d? Whoops.</p>

<p><a href="https://github.com/cheddar-me/gouda/blob/main/lib/active_job/queue_adapters/gouda_adapter.rb">Same for ActiveJob.</a> <a href="https://github.com/rails/solid_cache/blob/main/lib/active_support/cache/solid_cache_store.rb">Same for cache stores.</a></p>

<p>With ActiveJob and cache stores the situation is actually even worse. The module gets resolved not by string inclusion (which at least allows you to grep for things), but with an additional transformation to de-underscore your module name. For example, to configure Gouda in our app, we need to do this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">config</span><span class="p">.</span><span class="nf">active_job</span><span class="p">.</span><span class="nf">queue_adapter</span> <span class="o">=</span> <span class="ss">:gouda</span>
</code></pre></div></div>

<p>and we need to know that <code class="language-plaintext highlighter-rouge">:gouda</code> will Somehow Magically™ resolve to <code class="language-plaintext highlighter-rouge">ActiveJob::QueueAdapters::GoudaAdapter</code>, which we need to have in place before trying. Note that having to be inside a private Rails namespace has issues too - for example, Ruby’s constant lookup (which is sometimes-lexically-scoped-and-sometimes-not) changes subtly if your module is brought into another namespace. You may need to start using module names with their full qualification where you previously did not have to, and so forth.</p>

<p>Now, doing <em>lazy registration</em> - resolving those modules after the application has booted - has its very legitimate reasons. For example, in your application config:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">config</span><span class="p">.</span><span class="nf">anonymize_database_columns</span> <span class="o">=</span> <span class="no">ColumnAnonymizer</span><span class="p">.</span><span class="nf">columns_to_anonymize</span>
</code></pre></div></div>

<p>would imply that the <code class="language-plaintext highlighter-rouge">ColumnAnonymizer</code> may go into the database and scan it for all columns called <code class="language-plaintext highlighter-rouge">ssn</code>, <code class="language-plaintext highlighter-rouge">address</code>, <code class="language-plaintext highlighter-rouge">iban</code> and so forth. This would blow up, because at configuration load multiple parts of the app are still not “live” - there may be no database connection yet. Or there may be a database connection, but no database created - like with a freshly checked out codebase. So it is, in general, a good idea to defer the actual calls into the registered modules until the time they are actually needed - or, more precisely, until the time that these modules can produce a meaningful result from getting called. Modules may also interact with each other (include each other’s submodules, require base classes from each other and so forth), which is something that is better deferred until loading has reached some stable state.</p>

<p>But there is zero reason whatsoever to demand the modules you use be in private Rails namespaces. Zero. How this could be solved? Quite easily (but please, seriously - we need to rename this <code class="language-plaintext highlighter-rouge">class_name</code> because it isn’t):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">KNOWN_SERVICES</span> <span class="o">=</span> <span class="p">{}</span>

<span class="k">def</span> <span class="nf">register3</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="n">service_class_name</span><span class="p">)</span>
  <span class="no">KNOWN_SERVICES</span><span class="p">[</span><span class="nb">name</span><span class="p">.</span><span class="nf">to_sym</span><span class="p">]</span> <span class="o">=</span> <span class="n">service_class_name</span><span class="p">.</span><span class="nf">to_s</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">resolve</span><span class="p">(</span><span class="nb">name</span><span class="p">)</span>
  <span class="n">class_name</span> <span class="o">=</span> <span class="no">KNOWN_SERVICES</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="nb">name</span><span class="p">.</span><span class="nf">to_sym</span><span class="p">)</span> <span class="k">do</span>
    <span class="nb">require</span> <span class="s2">"active_storage/service/</span><span class="si">#{</span><span class="n">class_name</span><span class="p">.</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">underscore</span><span class="si">}</span><span class="s2">_service"</span>
    <span class="s2">"ActiveStorage::Service::</span><span class="si">#{</span><span class="nb">name</span><span class="p">.</span><span class="nf">camelize</span><span class="si">}</span><span class="s2">Service"</span>
  <span class="k">end</span>
  <span class="n">class_name</span><span class="p">.</span><span class="nf">safe_constantize</span>
<span class="k">rescue</span> <span class="no">LoadError</span>
  <span class="k">raise</span> <span class="o">&lt;&lt;~</span><span class="no">MSG</span><span class="sh">
    Missing service adapter for </span><span class="si">#{</span><span class="nb">name</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="sh">
    Known adapters are </span><span class="si">#{</span><span class="no">KNOWN_SERVICES</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">", "</span><span class="p">)</span><span class="si">}</span><span class="sh">
</span><span class="no">  MSG</span>
<span class="k">end</span>
</code></pre></div></div>

<p>That way, an external gem could call the following line from its <code class="language-plaintext highlighter-rouge">Railtie</code> and be done:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">ActiveStorage</span><span class="o">::</span><span class="no">Service</span><span class="p">.</span><span class="nf">register</span><span class="p">(</span><span class="s2">"Blockchain"</span><span class="p">,</span> <span class="s2">"Web3::ActiveStorageService"</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="in-summary">In summary</h2>

<p>Module registration is not something out of “Enterprise Application Architecture With Java Version 1.0” tome that is collecting dust under your desk. It is a very useful pattern for making good use of polymorphism. It is both useful in the OOP world and in the FP world. It is also useful in the in the strictly-typed world, where your module just needs to satisfy a type constraint. When you will need to grab for it - hope these tips can be of use.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Having the same architecture problems over and over does give you perspective. We all love making fun of the enterprise FizzBuzz but there are cases where those Factories, Adapters and Facades are genuinely very useful, and so is dependency injection. Since I had to do dependency injection combined with adapters a wee many times now, it seems like a good idea to share my experience. What I will describe here mostly applies to Ruby, but it mostly applies to the other languages and runtimes too.]]></summary></entry><entry><title type="html">The surcharge of big tech</title><link href="https://blog.julik.nl/2025/02/the-surcharge-of-big-tech" rel="alternate" type="text/html" title="The surcharge of big tech" /><published>2025-02-09T00:00:00+00:00</published><updated>2025-02-09T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/02/the-surcharge-of-big-tech</id><content type="html" xml:base="https://blog.julik.nl/2025/02/the-surcharge-of-big-tech"><![CDATA[<p>There is a lot of talk that big-tech companies are willing to pay way more, way up north of the market to the local rates. They all seem similar:</p>

<ul>
  <li>Pre-IPO or public</li>
  <li>Looking for senior software engineers or staff engineers</li>
  <li>Salary brackets never published, and even recruiters stay fairly tight lipped</li>
  <li>So-described “transparent” interview process - usually a marathon of systems design, “culture fit” and leetcode-like excercises</li>
</ul>

<p>And yet it seems that it is hard for those firms to acquire talent, even though in some cases they are prepared to offer compensation 40% to 50% higher than a standard local development agency would. Why is that?</p>

<p>Well, they know what they are recruiting for. It is a challenging environment, and – despite it sometimes lookign otherwise – they want the people they hire to still be able to perform.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>Just a few pathological examples of what can be encountered (all of these are not invented - I know about companies that actually do this):</p>

<ul>
  <li>Multiple teams working on conflicting implementations of the same feature</li>
  <li>A reorganization every 4 - 6 months, with a team reshuffle and change of manager attached</li>
  <li>Founders quitting / selling and new C-level leadership not caring about end-user experience anymore</li>
  <li>Continuous hire-above for leadership roles, bypassing even the most senior employees in the organization</li>
  <li>Obsession with “change for the sake of change” (because change is the only constant?…)</li>
  <li>Thriving <a href="https://defmacro.substack.com/p/how-to-get-promoted">opportunists</a></li>
  <li><a href="https://twitter.com/GergelyOrosz/status/1442162670753431559?s=19">Promotion-driven development</a></li>
  <li>That for every aspect of change that impacts you there would be somebody else appointed to make the decision, without any input from your side</li>
</ul>

<p>The end-result is that the folks Big Tech are looking for <em>indeed</em> are very expensive, because they are aware that “creating things” or “building things” or “maintaining things” is just a part of the story. Imagine this setup at an agency for example: your work output is a sum of</p>

<ul>
  <li>Navigating client relationship - 20%</li>
  <li>Communication/collaboration with peers - 40%</li>
  <li>Building/shipping - 40%</li>
</ul>

<p>Now let’s transpose the same performance of the same individual into a Big Tech context:</p>

<ul>
  <li>Navigating client relationships - 0%</li>
  <li>Dealing with rules and restrictions (compliance, SOX, restrictive systems access, ops team etc.) - 5%</li>
  <li>Dealing with politics - 15%</li>
  <li>Coordinating the team - 30%</li>
  <li>Coordinating across teams - 30%</li>
  <li>Building/shipping - 20%</li>
</ul>

<p>Depending on how exactly <a href="https://jonpauluritis.com/articles/all-companies-are-fucked-up/">your particular big tech co is extraordinary</a> the proportions may change, of course.</p>

<p>So you can see the delta between a Big Tech salary and an agency salary as a <strong>bullshit surcharge</strong> of sorts. Provided your main output is things you build or ship, that is. Big Tech companies do realize that working for them does include those challenges. Not because they are bad collectives of people, and not because they are somehow “flawed” - but because large numbers of groups of people necessarily create certain phenomena that you cannot just “cancel away”. And they budget accordingly.</p>

<p>And for a good reason: every senior candidate they are interviewing is going to try to gauge not only whether the “base salary segment” is fair, but also the amount of bureaucracy and politics they are going to be dealing with. Is there a whiff of a toxic culture? How recently was the team formed? How many reorganizations have there been lately? What is the reorganization cadence? What kinds of restrictions exist?</p>

<p>And more importantly: people who are great at working in big tech (and by saying it I mean it: this is not meant sarcastically) are not only able to navigate this type of environment – they are able to <em>ship</em> in that environment and make meaningful contributions to the business <em>despite</em> the inherent toxicity of it all. And be optimistic and kind while doing so. This is the kind of person the actual big tech positions want to acquire – only it rarely will be written in a job description.</p>

<p>And they are ready to pay accordingly.</p>

<p>What is really important to understand, is that “mid-tech” (up to, say, 150 employees) or even small tech can meaningfully tip the scales in its favor by openly advertising all the things a big tech firm simply can no longer afford.</p>

<p>Just a small sampler:</p>

<ul>
  <li>No leetcode interviews, we will actually look at yout Github profile, writing and presentations instead</li>
  <li>No unpaid test assignments</li>
  <li>No JIRA will be used in the organization for N years (CEO guarantee)</li>
  <li>Agile/Scrum will not be introduced in the organization for N years (CEO guarantee)</li>
  <li>After some time of tenure you get admin access to key systems you work on, no questions asked</li>
  <li>Engineers are expected to provide meaningful product input</li>
</ul>

<p>etc etc. Smaller employers still make a measurable chance, because if you remove the bullshit you won’t need to pay the bullshit surcharge anymore. But the kind of people you are likely to want to have are different than the ones who are well oiled for success with big tech, too:</p>

<ul>
  <li>Expects a very high degree of agency</li>
  <li>Strong opinions, loosely held</li>
  <li>High operational excellence</li>
  <li>Creativity in shaping process where no process exists</li>
</ul>

<p>Look for those, and you may not only make more progress than big tech, but also get to know people who would never, ever go to work for big tech – and will work for you instead. And they would fit with you, and fit better. Exactly because <em>you are not it.</em></p>

<p>At the big tech, <a href="https://x.com/signulll/status/1887663399615467794">they are still moving the button by 5 pixels.</a></p>

<hr />

<p>A few articles on this by Sean Goedecke which align perfectly with my thoughts on the topic:</p>

<ul>
  <li><a href="https://www.seangoedecke.com/difficulty-in-big-tech/">Why are big tech companies so slow</a></li>
  <li><a href="https://www.seangoedecke.com/predators/">Protecting your time from predators in large tech companies</a></li>
  <li><a href="https://www.seangoedecke.com/how-to-ship/">How I ship projects at big tech companies</a></li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[There is a lot of talk that big-tech companies are willing to pay way more, way up north of the market to the local rates. They all seem similar: Pre-IPO or public Looking for senior software engineers or staff engineers Salary brackets never published, and even recruiters stay fairly tight lipped So-described “transparent” interview process - usually a marathon of systems design, “culture fit” and leetcode-like excercises And yet it seems that it is hard for those firms to acquire talent, even though in some cases they are prepared to offer compensation 40% to 50% higher than a standard local development agency would. Why is that? Well, they know what they are recruiting for. It is a challenging environment, and – despite it sometimes lookign otherwise – they want the people they hire to still be able to perform.]]></summary></entry><entry><title type="html">Disownership, pull requests and de-facto architects</title><link href="https://blog.julik.nl/2025/01/disownership-and-pull-request-reviews" rel="alternate" type="text/html" title="Disownership, pull requests and de-facto architects" /><published>2025-01-25T00:00:00+00:00</published><updated>2025-01-25T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/01/disownership-and-pull-request-reviews</id><content type="html" xml:base="https://blog.julik.nl/2025/01/disownership-and-pull-request-reviews"><![CDATA[<p>A while ago I got really triggered by by the following two tweets - <a href="https://twitter.com/avdi/status/1376928208386412551">one by Avdi</a>  and <a href="https://twitter.com/ph1/status/1370410395328913408">another by Pete</a></p>

<p>This essay was on my mind - and lying dormant - for a couple of years, but I think it didnt lose its relevance.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>I must admit that I have been struggling with this a ton, and while the tweets do outline the problem I think we are missing out on a very important component. We’ve all seen more than our fair share of systems that were “difficult to work with”, or “too large to keep in one’s head”, or “too complex and needing focus to understand”. In a few instances there was indeed a lot of complexity present, usually because a system was very tied to a specific piece of hardware or an OS primitive: for instance, how do you ensure your OS really really does <code class="language-plaintext highlighter-rouge">fsync()</code> at the right point? What are the failure modes when this gets used with NFS? During shutdown? During a progressing storage media failure? and the like. Or when a certain computation neds to be optimized to the maximum, and you really need to take a lot of the minutiae of your language and runtime into account to get there?</p>

<p>But most of the “complex systems” I’ve encountered came not in the shape of being “complex” but of being “complicated”, or rather <em>overcomplicated.</em> They evolved into being complicated because there was, knowingly or unknowingly, a process of “complicationing” them throughout their lifetime. Very often these systems can be characterised by actually making money for the owning business too! since they have survived a number of organisational evolutions and transformations, changes in direction, changes in scope, changes in runtime or changes in personnel working on them. With a lot of these “complicated” systems there was an incredible amount of value to be had from analyzing the political context in the organization at the time those systems evolved or emerged. Very often this context can be second-guessed from the Git commit history. If you examine an application which has served for long enough, you will invariably see a few key names in the commits - those were the de-facto architects of that system.</p>

<p>Formally or informally, it were them who have shaped the direciton of the system going forward, and have reacted to the organizational tantrums and undercurrents. Sometimes they left, like ancient civilizations whose scriptiures and wall carvings we admire so much. Sometimes they persevered, and stuck to the bitter end (the last example being a - to me - good natured trend of <a href="https://github.com/campsite/campsite">companies making their source code available when going out of business.</a>)</p>

<p>What I did see happen, though, was that the system - if it is at least somewhat successful - outlives the tenure of those de-facto architects at the company. It will evolve, change, mutate, transform in different ways - but the end outcome would be similar, the system would be passed on to a new hire (or a team of new hires) who would then in turn become the de-facto architects. People do move on, after all.</p>

<p>This process is a living embodiment of today’s concept of “shipping your org chart”. At a certain moment person X was in charge of a system, and later on person Y took over from person X. Then person Y got fired over a scandal, or moved on for an interesting position at a FAANG, or got finally offered a coveted CTO title at a startup where they could call the shots. Or left to build a cabin in the Swedish forests. The outcome though:  the resulting, “de facto” design of the system will then also be a reflection of the passions and tribulations of that person. If you scan through the commit logs of an application which has existed for a while, you will often see certain milestones, for example:</p>

<ul>
  <li>Here person X has discovered service objects</li>
  <li>Here person Y tried to implement CQRS</li>
  <li>Here person Z attempted to use namespacing, but found out that ActiveRecord makes it quite miserable to use</li>
  <li>Here person W got hired and they tried to change everything to use Trailbazer, but their commits abruptly stop after 1 month and people shy away when you ask them about W.</li>
  <li>Here a part of a system written by person N had to be replaced, but person N was still at the company - so instead of integrating that system into the new code a bridge had to be written as to not make person N upset (and they would get very upset if their system was removed).</li>
  <li>Here an integration with an external system had to be present, but because the product management was rushed there was no local caching</li>
</ul>

<p>Although we all may strive for “collective code ownership” factually it will never, ever materialize. Even in Soviet Russia there was no “collective art”. Yes, you can have a collective artwork of 2 or 3 artists who have willingly decided to collaborate on something (a rare but incredible occurrence!) but they chose to get together out of their own volition! People hired into an organization, by different managers, at different developmental stretches as the org matures, are very unlikely to end up in a situation of spontaneous shared creativity. Spontaneous self-immolation might be more likely I’d say, especially if both of them hold opinions™.</p>

<p>And this is where we get to the crux of the issue. That difficult person “imposing their style” in a PR review – ask them about a couple of the milestones you saw in the codebase. How did they come to be? Was this person consulted? Were there overriding orders? Was there an extreme deadline imposed somewhere? is there a reference to one of those milestones in the review they left under your code?</p>

<p>If you dig deep enough, it will often become apparent that a lot of the situations where people are overbearing in code reviews - or have to deal with a complicated system and focus to “load it into their L3 mental cache” is because, well… what if they <em>did</em> do a lot as to make that system less complicated? What if they <em>did</em> flag it that an integration with an external service exponentially increases the code paths you have to cover? What if they <em>did</em> flag it that implementing a certain feature “without UI” was discarding a very specific, tangible use case for which now there is a 2K-line workaround in place?</p>

<p>The outcome of this conversation might surprise you, or change your perspective on “that difficult guy”. Yes, it could have been an asshole - but it could have, just as well, been a genuinely caring and forward-thinking person.</p>

<p>Also note that this political context is largely taboo for a group conversation, both in Western and in Eastern cultures. It can only be broken in a super-aligned, cohesive team (one might even say that this team would be a group-think-team) enjoying an incredible amount of trust between members.</p>

<p>Consistently, from asking a few people who have been in those situations, but also from having been in those situations myself, a pattern was always present that a person <em>had to</em> be the de-facto architect, but with going into that role there is another card attached - the power to say “no”. And at some key junctions of the evolution of the org, this power was not available. Or taken away. Or never given to begin with. So in a way, you are not an “owner” of something but a “custodian”, and any “new” person or current (“Let’s use GraphQL for everything”, “Let’s rewrite everything from PHP to Ruby on Rails”, “Let’s use Redux everywhere”) may come into the garden under your custodianship and trample on all the vegetation you have tried so hard to preserve. It is not an enviable position to be in, and exactly one of the gut reactions to this is being a dick about “small things”. It comes from a position of insecurity. Because you know that nobody is going to listen to things you say about topics that <em>really matter</em> and they are not going to be addressed - you revert to, at least “in the small”, to the “if we keep just this rose, just in this little corner”, you might be able to prevent a nuclear disaster or a problem down the line. Or at least feel like you are able to perform your work with dignity – it can give you an illusion of having autonomy and control.</p>

<p>There are a few key forces driving this process. Firstly, organizations want to be independent of a creative professional - yet they consider themselves fully entitled to the fruits of that professional’s creativity. Secondly, nobody can guarantee that such a professional will be able to stay aligned with the organization along its evolution. Yesterday we had to “not be evil”, today we have to prioritize adverts for competing products over a product’s website. With the incredible amount of money flowing through the ecosystem it is only fitting that if, say, such a key person would decide to “say no” at that crucial moment (a new investment round maybe? a dilution? <a href="https://dropdropbox.com">appointing a politician implicated with authorizing torture</a> to your board of directors?) the business may be in jeopardy. I am deliberately picking some very extreme examples here and specifically hammering on ethics, very often the disputes will be much smaller and the “right side to be on” – much harder to pick. But the disputes will be there. Naturally, an organization will try to de-risk operation by removing as much authority from such a creative professional as possible, and make no mistake: making decisions about how a system evolves is also authority. Deny a person their voice enough times, and they will start behaving like a jerk. Or leave.</p>

<p>So don’t be surprised if you end up with a spaghetti monster instead of a clean design, and please do avoid generalisations about “people who do X also always do Y”. Maybe the actual problem is that we are so scarred by the stories of “John is the only one who may touch X” that we are denying software authors ownership or consultation? And that is where our overcomplicated systems come from?</p>

<p>Note that this is specifically not one of the other sutuations where pull-requests are useful even outside async, open-source work in low-trust environments:</p>

<ul>
  <li>Critical code paths or spots where race conditions could occur, potentially leading to data loss or financial consequences</li>
  <li>Sketching out designs together (whereby design is moving further than a wireframe)</li>
</ul>

<p>Of course, what I am describing <em>is</em> inherently a “low trust” environment, as Avdi duly notes - but this is exactly how people lose trust in each other.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[A while ago I got really triggered by by the following two tweets - one by Avdi and another by Pete This essay was on my mind - and lying dormant - for a couple of years, but I think it didnt lose its relevance.]]></summary></entry><entry><title type="html">Supercharge SQLite with Ruby functions</title><link href="https://blog.julik.nl/2025/01/supercharge-sqlite-with-ruby-functions" rel="alternate" type="text/html" title="Supercharge SQLite with Ruby functions" /><published>2025-01-23T00:00:00+00:00</published><updated>2025-01-23T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/01/supercharge-sqlite-with-ruby-functions</id><content type="html" xml:base="https://blog.julik.nl/2025/01/supercharge-sqlite-with-ruby-functions"><![CDATA[<p>An interesting twist in my recent usage of SQLite was the fact that I noticed my research scripts and the database intertwine more. SQLite is unique in that it really lives in-process, unlike standalone database servers. There is a feature to that which does not get used very frequently, but can be indispensable in some situations.</p>

<p>By the way, the talk about the system that made me me to explore SQLite in anger can now be <a href="https://www.youtube.com/embed/q_EbeIaH0xw?list=PLZW-kXE0oRykbAG33qUp5izazF3wvHcft">seen here.</a></p>

<p>Normally it is your Ruby (or Python, or Go, or whatever) program which calls SQLite to make it “do stuff”. Most calls will be mapped to a native call like <code class="language-plaintext highlighter-rouge">sqlite3_exec()</code> which will do “SQLite things” and return you a result, converted into data structures accessible to your runtime. But there is another possible direction here - SQLite can actually call your code instead.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>There is no support for stored procedures in SQLite (which is imaginable, since the database already lives inside of a very large stored procedure - your program, basically). But: in addition to stored procedures, databases sometimes support something called <code class="language-plaintext highlighter-rouge">UDF</code>s, which stands for <em>User-Defined Functions.</em> Most SQL databases have some functions built in:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">RANDOM</span><span class="p">()</span>
</code></pre></div></div>

<p>calls a built-in SQL function called <code class="language-plaintext highlighter-rouge">RANDOM</code>, which will accept no arguments and return you a random integer. A call like this:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="k">LOWER</span><span class="p">(</span><span class="s1">'HELLO'</span><span class="p">)</span>
</code></pre></div></div>

<p>calls a built-in SQL function called <code class="language-plaintext highlighter-rouge">LOWER</code>, which accepts one argument and returns that argument converted to lowercase. The beauty of it is, of course, that it does not have to be a literal - it can be a column resulting from a different part of the SQL statement:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">id</span><span class="p">,</span> <span class="k">UPPER</span><span class="p">(</span><span class="n">legal_name</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">users</span>
</code></pre></div></div>

<p>will select you the user names converted to uppercase. Important to note: these functions are what’s called <em>scalar</em> functions - they map a piece of data from a single result row to a piece of output. Another family of SQL functions are <em>aggregate</em> functions, which accept the entire result set and apply to all values in that dataset. These are your <code class="language-plaintext highlighter-rouge">MIN</code> and <code class="language-plaintext highlighter-rouge">MAX</code> functions:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="k">MAX</span><span class="p">(</span><span class="n">age</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">users</span>
</code></pre></div></div>

<p>They map values from multiple rows to one piece of output. Same for <code class="language-plaintext highlighter-rouge">SUM</code>.</p>

<p>The builtin functions in SQLite are pretty neat, but the menu is not that large, and sometimes exactly the function we need is not on that menu, sadly. In most DB systems you can define functions in the language the database supports - usually it will be some kind of superset of SQL the database supports, or one of the scripting languages the database embeds. With SQLite, however, something much nicer is possible: you can expose your own code from your “main” program and make it available to your SQL queries. And in some situations it can be very handy.</p>

<h3 id="creating-your-own-scalar-udf">Creating your own scalar UDF</h3>

<p>For instance: at Cheddar we use a small library called <a href="https://github.com/cheddar-me/tou">tou</a> - which is a flavour of time-ordered UUIDs. Let’s make it available to our database:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">users</span> <span class="p">(</span>
  <span class="n">uid</span> <span class="nb">VARCHAR</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>
  <span class="n">name</span> <span class="nb">VARCHAR</span> <span class="k">NOT</span> <span class="k">NULL</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Then, let’s create an enumerator which will issue us a deterministic sequence of Tou IDs, all sorted to the same timestamp:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">id_gen</span> <span class="o">=</span> <span class="no">Enumerator</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">yielder</span><span class="o">|</span>
  <span class="n">time</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">at</span><span class="p">(</span><span class="mf">1737665388.5792</span><span class="p">)</span>
  <span class="n">random</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
  <span class="kp">loop</span> <span class="k">do</span>
    <span class="n">yielder</span><span class="p">.</span><span class="nf">yield</span><span class="p">(</span><span class="no">Tou</span><span class="p">.</span><span class="nf">uuid</span><span class="p">(</span><span class="n">random</span><span class="p">:,</span> <span class="n">time</span><span class="p">:))</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The nice thing about this Enumerator is that we can now grab us a fresh Tou UUID by calling <code class="language-plaintext highlighter-rouge">next</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="mi">13</span><span class="p">]</span> <span class="n">pry</span><span class="p">(</span><span class="n">main</span><span class="p">)</span><span class="o">&gt;</span> <span class="n">id_gen</span><span class="p">.</span><span class="nf">next</span>
<span class="o">=&gt;</span> <span class="s2">"062c65c0-3989-4806-9ce1-5fb33deacb5c"</span>
<span class="p">[</span><span class="mi">14</span><span class="p">]</span> <span class="n">pry</span><span class="p">(</span><span class="n">main</span><span class="p">)</span><span class="o">&gt;</span> <span class="n">id_gen</span><span class="p">.</span><span class="nf">next</span>
<span class="o">=&gt;</span> <span class="s2">"062c65c0-3989-480e-95f5-2e6af463bb47"</span>
</code></pre></div></div>

<p>We can insert users into our table with those IDs like so:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="s2">"INSERT INTO users (uid, name) VALUES (?, ?)"</span><span class="p">,</span> <span class="p">[</span><span class="n">id_gen</span><span class="p">.</span><span class="nf">next</span><span class="p">,</span> <span class="s2">"John"</span><span class="p">])</span>
</code></pre></div></div>

<p>and it will work, but sometimes we would want those values to be provided by SQLite itself. A good example is when doing a <code class="language-plaintext highlighter-rouge">UNION ALL</code> select to combine multiple datasets:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="s1">'Legal drinking age'</span> <span class="k">AS</span> <span class="k">desc</span><span class="p">,</span> <span class="k">COUNT</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="k">AS</span> <span class="n">cnt</span> <span class="k">FROM</span> <span class="n">users</span> <span class="k">WHERE</span> <span class="n">age</span> <span class="o">&gt;=</span> <span class="mi">21</span>
<span class="k">UNION</span> <span class="k">ALL</span> <span class="k">SELECT</span> <span class="s1">'Below legal drinking age'</span> <span class="k">AS</span> <span class="k">desc</span><span class="p">,</span> <span class="k">COUNT</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="k">AS</span> <span class="n">cnt</span> <span class="k">FROM</span> <span class="n">users</span> <span class="k">WHERE</span> <span class="n">age</span> <span class="o">&lt;</span> <span class="mi">21</span>
</code></pre></div></div>

<p>If this is for a report this is fine, but if we want to retain our result rows and address them, we may want to give them some kind of ID. Because we are letting the database produce us a dataset - using placeholders here is not possible. However, we can tell SQLite to generate those IDs by itself, for every row of the <code class="language-plaintext highlighter-rouge">SELECT</code> result sets:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">TOU</span><span class="p">()</span> <span class="k">AS</span> <span class="n">id</span><span class="p">,</span> <span class="s1">'Legal drinking age'</span> <span class="k">AS</span> <span class="k">desc</span><span class="p">,</span> <span class="k">COUNT</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="k">AS</span> <span class="n">cnt</span> <span class="k">FROM</span> <span class="n">users</span> <span class="k">WHERE</span> <span class="n">age</span> <span class="o">&gt;=</span> <span class="mi">21</span>
<span class="k">UNION</span> <span class="k">ALL</span>
<span class="k">SELECT</span> <span class="n">TOU</span><span class="p">()</span> <span class="k">AS</span> <span class="n">id</span><span class="p">,</span> <span class="s1">'Below legal drinking age'</span> <span class="k">AS</span> <span class="k">desc</span><span class="p">,</span> <span class="k">COUNT</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="k">AS</span> <span class="n">cnt</span> <span class="k">FROM</span> <span class="n">users</span> <span class="k">WHERE</span> <span class="n">age</span> <span class="o">&lt;</span> <span class="mi">21</span>
</code></pre></div></div>

<p>To be able to do that, we need to expose our little ID generator to SQLite. It is done like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">db</span><span class="p">.</span><span class="nf">create_function</span><span class="p">(</span> <span class="s2">"TOU"</span><span class="p">,</span> <span class="n">_arity</span> <span class="o">=</span> <span class="mi">0</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">func</span><span class="o">|</span>
  <span class="n">func</span><span class="p">.</span><span class="nf">result</span> <span class="o">=</span> <span class="n">id_gen</span><span class="p">.</span><span class="nf">next</span>
<span class="k">end</span>
</code></pre></div></div>

<p>We create a closure which addresses our ID generator and ratchets it to a new value. The value then gets assigned to the result of the function.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="mi">22</span><span class="p">]</span> <span class="n">pry</span><span class="p">(</span><span class="n">main</span><span class="p">)</span><span class="o">&gt;</span> <span class="n">db</span><span class="p">.</span><span class="nf">get_first_value</span><span class="p">(</span><span class="s2">"SELECT TOU()"</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="s2">"062c65c0-3989-480c-ae41-99142ccb9866"</span>
</code></pre></div></div>

<p>and now we can auto-ID our rows.</p>

<p>Another great use for scalar functions: doing some operation that you need to happen in your main runtime, and examining the results of this operation in bulk. Imagine we want to do some regex matching, but either SQLite does not have the regexp extension (is of an older version), or - which is something that actually happened to me - you want to test a Ruby regexp against all the items in your dataset. Regex engines are different in implementation and in their quirks. If you are designing regexes and you intend to run them in your Ruby runtime later, it can be very useful to test them against a dataset that you already have. So, let’s design a function which will just tell us whether a passed string matches a Ruby regexp:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">db</span><span class="p">.</span><span class="nf">create_function</span><span class="p">(</span> <span class="s2">"TEXT_MATCHES_RE"</span><span class="p">,</span> <span class="n">_arity</span> <span class="o">=</span> <span class="mi">2</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">func</span><span class="p">,</span> <span class="n">text</span><span class="p">,</span> <span class="n">regexp_str</span><span class="o">|</span>
  <span class="n">re</span> <span class="o">=</span> <span class="no">Regexp</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">regexp_str</span><span class="p">)</span>
  <span class="n">func</span><span class="p">.</span><span class="nf">result</span> <span class="o">=</span> <span class="n">text</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">match?</span><span class="p">(</span><span class="n">re</span><span class="p">)</span> <span class="p">?</span> <span class="mi">1</span> <span class="p">:</span> <span class="mi">0</span> <span class="c1"># SQLite has no boolean type</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Note how I word the name of the function in such a way that would hint the order of arguments (string to match first, regex pattern second). Let’s run it:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="mi">30</span><span class="p">]</span> <span class="n">pry</span><span class="p">(</span><span class="n">main</span><span class="p">)</span><span class="o">&gt;</span> <span class="n">db</span><span class="p">.</span><span class="nf">get_first_value</span><span class="p">(</span><span class="s2">"SELECT TEXT_MATCHES_RE('john', '[a-z]')"</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="mi">1</span>
<span class="p">[</span><span class="mi">31</span><span class="p">]</span> <span class="n">pry</span><span class="p">(</span><span class="n">main</span><span class="p">)</span><span class="o">&gt;</span> <span class="n">db</span><span class="p">.</span><span class="nf">get_first_value</span><span class="p">(</span><span class="s2">"SELECT TEXT_MATCHES_RE('123', '[a-z]')"</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="mi">0</span>
</code></pre></div></div>

<p>Since SQLite treats 1 and 0 as booleans we can use our new function in a <code class="language-plaintext highlighter-rouge">WHERE</code> clause (which is the main spot this kind of UDF will be useful):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="s2">"CREATE TABLE users (name TEXT NOT NULL)"</span><span class="p">);</span>
<span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="s2">"INSERT INTO users (name) VALUES (?), (?), (?), (?)"</span><span class="p">,</span> <span class="p">[</span><span class="s2">"Jane"</span><span class="p">,</span> <span class="s2">"Bobby"</span><span class="p">,</span> <span class="s2">"Jake"</span><span class="p">,</span> <span class="s2">"Peter"</span><span class="p">]);</span>
<span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="s2">"SELECT * FROM users WHERE TEXT_MATCHES_RE(name, '^Ja')"</span><span class="p">).</span><span class="nf">to_a</span> <span class="c1"># =&gt; [["Jane"], ["Jake"]]</span>
</code></pre></div></div>

<h3 id="rolling-it-up-with-aggregates">Rolling it up with aggregates</h3>

<p>When I was working on Sked - the scheduling simulator - one of the most useful things was rapidly doing statistical queries after the simulation ran. Doing things like computing the percentiles, the standard deviation and the like. While SQLite does have <code class="language-plaintext highlighter-rouge">MIN</code> and <code class="language-plaintext highlighter-rouge">MAX</code>, it does not have the more advanced functions that may be useful in those scenarios - like <a href="https://www.postgresql.org/docs/current/functions-aggregate.html">percentile_cont</a> - luckily, though, these can be implemented right inside the script used for experimenting.</p>

<p>An aggregate function in SQLite can be defined by supplying callbacks, but the Ruby SQLite library offers another option - defining your aggregate function as a class which gets instantiated. The instance holds the state that needs to be kept while the aggregate function gets called (the so-called “step”) for every value in the resultset.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">StddevSampHandler</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">arity</span> <span class="o">=</span> <span class="mi">1</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">name</span> <span class="o">=</span> <span class="s2">"stddev_samp"</span>

  <span class="k">def</span> <span class="nf">initialize</span>
    <span class="vi">@sample</span> <span class="o">=</span> <span class="p">[]</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">step</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
    <span class="vi">@sample</span> <span class="o">&lt;&lt;</span> <span class="n">value</span><span class="p">.</span><span class="nf">to_f</span> <span class="k">if</span> <span class="n">value</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">finalize</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
    <span class="k">if</span> <span class="vi">@sample</span><span class="p">.</span><span class="nf">length</span> <span class="o">&gt;</span> <span class="mi">1</span>
      <span class="n">mean</span> <span class="o">=</span> <span class="vi">@sample</span><span class="p">.</span><span class="nf">sum</span><span class="p">.</span><span class="nf">to_f</span> <span class="o">/</span> <span class="vi">@sample</span><span class="p">.</span><span class="nf">length</span>
      <span class="n">sd</span> <span class="o">=</span> <span class="vi">@sample</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">v</span><span class="o">|</span> <span class="p">(</span><span class="n">v</span> <span class="o">-</span> <span class="n">mean</span><span class="p">)</span><span class="o">**</span><span class="mi">2</span> <span class="p">}.</span><span class="nf">sum</span>
      <span class="n">ctx</span><span class="p">.</span><span class="nf">result</span> <span class="o">=</span> <span class="no">Math</span><span class="p">.</span><span class="nf">sqrt</span><span class="p">(</span><span class="n">sd</span> <span class="o">/</span> <span class="p">(</span><span class="vi">@sample</span><span class="p">.</span><span class="nf">length</span> <span class="o">-</span> <span class="mi">1</span><span class="p">))</span>
    <span class="k">else</span>
      <span class="n">ctx</span><span class="p">.</span><span class="nf">result</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">end</span>
    <span class="vi">@sample</span><span class="p">.</span><span class="nf">clear</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">PercentileHandler</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">arity</span> <span class="o">=</span> <span class="mi">1</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">name</span> <span class="o">=</span> <span class="s2">"p90"</span>

  <span class="k">def</span> <span class="nf">initialize</span>
    <span class="vi">@sample</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="vi">@fraction</span> <span class="o">=</span> <span class="mf">0.9</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">step</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
    <span class="vi">@sample</span> <span class="o">&lt;&lt;</span> <span class="n">value</span><span class="p">.</span><span class="nf">to_f</span> <span class="k">if</span> <span class="n">value</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">finalize</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
    <span class="k">if</span> <span class="vi">@sample</span><span class="p">.</span><span class="nf">length</span> <span class="o">&gt;</span> <span class="mi">1</span>
      <span class="vi">@sample</span><span class="p">.</span><span class="nf">sort!</span>
      <span class="n">k</span> <span class="o">=</span> <span class="p">(</span><span class="vi">@fraction</span> <span class="o">*</span> <span class="p">(</span><span class="vi">@sample</span><span class="p">.</span><span class="nf">length</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">).</span><span class="nf">floor</span> <span class="o">-</span> <span class="mi">1</span>
      <span class="n">f</span> <span class="o">=</span> <span class="p">(</span><span class="vi">@fraction</span> <span class="o">*</span> <span class="p">(</span><span class="vi">@sample</span><span class="p">.</span><span class="nf">length</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">).</span><span class="nf">modulo</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
      <span class="n">ctx</span><span class="p">.</span><span class="nf">result</span> <span class="o">=</span> <span class="vi">@sample</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">+</span> <span class="p">(</span><span class="n">f</span> <span class="o">*</span> <span class="p">(</span><span class="vi">@sample</span><span class="p">[</span><span class="n">k</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]</span> <span class="o">-</span> <span class="vi">@sample</span><span class="p">[</span><span class="n">k</span><span class="p">]))</span>
    <span class="k">else</span>
      <span class="n">ctx</span><span class="p">.</span><span class="nf">result</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">end</span>
    <span class="vi">@sample</span><span class="p">.</span><span class="nf">clear</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="n">db</span><span class="p">.</span><span class="nf">create_aggregate_handler</span><span class="p">(</span><span class="no">StddevSampHandler</span><span class="p">)</span>
<span class="n">db</span><span class="p">.</span><span class="nf">create_aggregate_handler</span><span class="p">(</span><span class="no">PercentileHandler</span><span class="p">)</span>
</code></pre></div></div>

<p>Then we can create our table of samples:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="s2">"CREATE TABLE metric_samples (name VARCHAR NOT NULL, value FLOAT)"</span><span class="p">)</span>
<span class="n">rng</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
<span class="n">values</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">11</span><span class="p">,</span> <span class="mi">12</span><span class="p">,</span> <span class="mi">12</span><span class="p">,</span> <span class="mi">25</span><span class="p">]</span>
<span class="n">metrics</span> <span class="o">=</span> <span class="sx">%w( latency duration )</span>
<span class="mi">199</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span>
  <span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="s2">"INSERT INTO metric_samples (name, value) VALUES (?, ?)"</span><span class="p">,</span> <span class="p">[</span><span class="n">metrics</span><span class="p">.</span><span class="nf">sample</span><span class="p">(</span><span class="ss">random: </span><span class="n">rng</span><span class="p">),</span> <span class="n">values</span><span class="p">.</span><span class="nf">sample</span><span class="p">(</span><span class="ss">random: </span><span class="n">rng</span><span class="p">)])</span>
<span class="k">end</span>
</code></pre></div></div>

<p>and query it:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="o">&lt;&lt;~</span><span class="no">SQL</span><span class="p">).</span><span class="nf">to_a</span><span class="sh">
SELECT
  COUNT(1) AS sample_size,
  name,
  printf("%.2f", MIN(value)) AS min,
  printf("%.2f", MAX(value)) AS max,
  printf("%.2f", AVG(value)) AS avg,
  printf("%.2f", stddev_samp(value)) AS stddev,
  printf("%.2f", p90(value)) AS p90
FROM metric_samples GROUP BY name
</span><span class="no">SQL</span>
</code></pre></div></div>

<p>which gives:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[
  [115, "duration", "1.00", "25.00", "6.10", "4.21", "12.00"],
  [84, "latency", "1.00", "25.00", "7.65", "5.78", "12.00"]
]
</code></pre></div></div>

<p>This turned out to be very useful when printing reports about the various simulations we ran for our queue workloads.</p>

<h3 id="a-stub-is-a-stub">A stub is a stub</h3>

<p>There is a caveat though. A SQL function can, normally, be used both in queries and in SQL statements run by the database itself. That may be triggers - which run on various changes to the tables, or the default values for columns. For example, nothing precludes us from creating a view with a <code class="language-plaintext highlighter-rouge">TOU()</code> value:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">VIEW</span> <span class="n">single_tou</span> <span class="k">AS</span> <span class="k">SELECT</span> <span class="n">TOU</span><span class="p">()</span>
</code></pre></div></div>

<p>and in our process, this will work just fine. However, an unpleasant surprise will await you if you try to open the database “outside” of your process - and portability is one of the great qualities of SQLite databases. When you open a database in <code class="language-plaintext highlighter-rouge">sqlite</code> or using your favourite GUI editor (I use <a href="https://www.sqlitepro.com/">SQLPro</a>) you will be greeted with an error if you try to <code class="language-plaintext highlighter-rouge">SELECT</code> from that view or examine it. Unlike other RDBMSes, the function in SQLite is just a “callback” which delegates work to your host application, and cannot be stored with the database - or executed without your application having defined the function.</p>

<p>When defining a function, there is a flag you can supply - <a href="https://sqlite.org/c3ref/c_deterministic.html#sqliteinnocuous">as per SQLite documentation</a> - which is called <code class="language-plaintext highlighter-rouge">SQLITE_DIRECTONLY</code> - which hints SQLite that your function should not be used in “retained” SQL statements, and may only be used via a direct call. The way to set that is a bit obtuse, but it is available. There is an argument called <code class="language-plaintext highlighter-rouge">text_rep</code> that these flags may be OR-d with. The default value tells SQLite that it is getting text data in UTF-8. Let’s define a <code class="language-plaintext highlighter-rouge">TOU2()</code> which will be direct-only:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">SQLITE_DIRECTONLY</span> <span class="o">=</span> <span class="mh">0x000080000</span>
<span class="n">text_rep_and_flags</span> <span class="o">=</span> <span class="no">SQLite3</span><span class="o">::</span><span class="no">Constants</span><span class="o">::</span><span class="no">TextRep</span><span class="o">::</span><span class="no">UTF8</span> <span class="o">|</span> <span class="no">SQLITE_DIRECTONLY</span>
<span class="n">db</span><span class="p">.</span><span class="nf">create_function</span><span class="p">(</span><span class="s2">"TOU2"</span><span class="p">,</span> <span class="n">_arity</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">text_rep_and_flags</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">func</span><span class="o">|</span>
  <span class="n">func</span><span class="p">.</span><span class="nf">result</span> <span class="o">=</span> <span class="n">id_gen</span><span class="p">.</span><span class="nf">next</span>
<span class="k">end</span>
<span class="n">db</span><span class="p">.</span><span class="nf">get_first_value</span><span class="p">(</span><span class="s2">"SELECT TOU2()"</span><span class="p">)</span> <span class="c1">#=&gt; "062c65c0-3989-480a-91de-0eca55917557"</span>
</code></pre></div></div>

<p>And we can create a view with that function, but if we try to <code class="language-plaintext highlighter-rouge">SELECT</code> from it, we will get an error:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="s2">"CREATE VIEW no_tou AS SELECT TOU2()"</span><span class="p">);</span>

<span class="n">db</span><span class="p">.</span><span class="nf">get_first_value</span><span class="p">(</span><span class="s2">"SELECT * FROM no_tou"</span><span class="p">)</span>
<span class="o">&gt;</span> <span class="no">SQLite3</span><span class="o">::</span><span class="no">SQLException</span><span class="p">:</span> <span class="n">unsafe</span> <span class="n">use</span> <span class="n">of</span> <span class="no">TOU2</span><span class="p">():</span>
<span class="o">&gt;</span> <span class="no">SELECT</span> <span class="o">*</span> <span class="no">FROM</span> <span class="n">no_tou</span>
</code></pre></div></div>

<p>This is at least better than having this view hang around - but something to keep in mind. <code class="language-plaintext highlighter-rouge">SQLITE_DETERMINISTIC</code> also seems handy.</p>

<h3 id="to-sqlite-and-back-again">To SQLite and back again</h3>

<p>Wiring Ruby methods into SQLite can give you some very interesting capabilities (doing HTTP requests from your SQLite calls, parsing HTML with Nokogiri, tokenizing and matching data) and are a great tool when exploring datasets. If you use a Ruby program to generate a SQLite database for later inspection, like I do - just mark them as <code class="language-plaintext highlighter-rouge">SQLITE_DIRECTONLY</code> to not be unpleasantly surprised. It would, of course, be wonderful to be able to “talk” to the SQLite database with the custom UDFs attached using a familiar GUI – but since the functions live in Ruby-land, it will be impractical.</p>

<p>Now go and make your <code class="language-plaintext highlighter-rouge">SELECT</code>ions.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[An interesting twist in my recent usage of SQLite was the fact that I noticed my research scripts and the database intertwine more. SQLite is unique in that it really lives in-process, unlike standalone database servers. There is a feature to that which does not get used very frequently, but can be indispensable in some situations. By the way, the talk about the system that made me me to explore SQLite in anger can now be seen here. Normally it is your Ruby (or Python, or Go, or whatever) program which calls SQLite to make it “do stuff”. Most calls will be mapped to a native call like sqlite3_exec() which will do “SQLite things” and return you a result, converted into data structures accessible to your runtime. But there is another possible direction here - SQLite can actually call your code instead.]]></summary></entry><entry><title type="html">Maximum speed SQLite inserts</title><link href="https://blog.julik.nl/2025/01/maximum-speed-sqlite-inserts" rel="alternate" type="text/html" title="Maximum speed SQLite inserts" /><published>2025-01-01T00:00:00+00:00</published><updated>2025-01-01T00:00:00+00:00</updated><id>https://blog.julik.nl/2025/01/maximum-speed-sqlite-inserts</id><content type="html" xml:base="https://blog.julik.nl/2025/01/maximum-speed-sqlite-inserts"><![CDATA[<p>In my work I tend to reach for <a href="https://sqlite.org">SQLite</a> more and more. The type of work I find it useful for most these days is quickly amalgamating, dissecting, collecting and analyzing large data sets. As I have outlined in my <a href="https://github.com/julik/talks/blob/master/euruko-2024-slot-scheduling/slot-scheduling.pdf">Euruko talk</a> on scheduling, a key element of the project was writing a simulator. That simulator outputs metrics - lots and lots of metrics, which resemble what our APM solution collects. Looking at those metrics makes it possible to plot, dissect and examine the performance of various job flows.</p>

<p>You can, of course, store those metrics in plain Ruby objects and then work with them in memory - there is nothing wrong with that. However, I find using SQL vastly superior. And since the simulator only ever runs on one machine, and every session is unique - SQLite is the perfect tool for collecting metrics. Even if it is not a specialized datastore.</p>

<p>One challenge presented itself, though: those metrics get output in very large amounts. Every tick of the simulator can generate thousands of values. Persisting them to SQLite is fast, but with very large amounts that “fast” becomes “not that fast”. I had to go through a number of steps to make these inserts more palatable, which led to a very, very pleasant speed improvement indeed. That seems worth sharing - so strap in and let’s play.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h3 id="setting-the-scene">Setting the scene</h3>

<p>Let’s generate our data first and see how far we can push our little setup.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rng</span> <span class="o">=</span> <span class="no">Random</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
<span class="n">metrics</span> <span class="o">=</span> <span class="sx">%w( foo bar baz bad bleg )</span>
<span class="n">values</span> <span class="o">=</span> <span class="p">(</span><span class="mi">500_000</span><span class="p">).</span><span class="nf">times</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">n</span><span class="o">|</span>
  <span class="p">{</span><span class="ss">name: </span><span class="n">metrics</span><span class="p">.</span><span class="nf">sample</span><span class="p">(</span><span class="ss">random: </span><span class="n">rng</span><span class="p">),</span> <span class="ss">value: </span><span class="n">rng</span><span class="p">.</span><span class="nf">rand</span><span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>We will assume we are inserting from <code class="language-plaintext highlighter-rouge">Hash</code> objects representing column-value mappings. We will use a fresh
database for every test and keep it in memory to not even care about the filesystem performance - for now:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">create_db</span>
  <span class="n">db</span> <span class="o">=</span> <span class="no">SQLite3</span><span class="o">::</span><span class="no">Database</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">":memory:"</span><span class="p">)</span>
  <span class="n">db</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="s2">"CREATE TABLE metrics (name VARCHAR NOT NULL, value FLOAT NOT NULL)"</span><span class="p">)</span>
  <span class="n">db</span>
<span class="k">end</span>
</code></pre></div></div>

<p>and add a timing helper:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">timed</span><span class="p">(</span><span class="o">&amp;</span><span class="n">blk</span><span class="p">)</span>
  <span class="n">t</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_MONOTONIC</span><span class="p">)</span>
  <span class="k">yield</span>
  <span class="n">delta</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_MONOTONIC</span><span class="p">)</span> <span class="o">-</span> <span class="n">t</span>
  <span class="nb">warn</span> <span class="s2">"Took </span><span class="si">#{</span><span class="n">delta</span><span class="si">}</span><span class="s2"> seconds"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>First - the naive insert:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">timed</span><span class="p">(</span><span class="s2">"Naive"</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">db</span> <span class="o">=</span> <span class="n">create_db</span>
  <span class="n">first_record</span> <span class="o">=</span> <span class="n">values</span><span class="p">.</span><span class="nf">first</span>
  <span class="n">cols</span> <span class="o">=</span> <span class="n">first_record</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">", "</span><span class="p">)</span>
  <span class="n">placeholders</span> <span class="o">=</span> <span class="p">([</span><span class="s2">"?"</span><span class="p">]</span> <span class="o">*</span> <span class="n">first_record</span><span class="p">.</span><span class="nf">length</span><span class="p">).</span><span class="nf">join</span><span class="p">(</span><span class="s2">", "</span><span class="p">)</span>
  <span class="n">sql</span> <span class="o">=</span> <span class="s2">"INSERT INTO metrics (</span><span class="si">#{</span><span class="n">cols</span><span class="si">}</span><span class="s2">) VALUES (</span><span class="si">#{</span><span class="n">placeholders</span><span class="si">}</span><span class="s2">)"</span>
  <span class="n">values</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">cols_to_values</span><span class="o">|</span>
    <span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="n">sql</span><span class="p">,</span> <span class="n">cols_to_values</span><span class="p">.</span><span class="nf">values</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This gives <code class="language-plaintext highlighter-rouge">Naive - 2.3065050000150222 seconds</code>. Surely we can do better than that. Transactions for bulk insert are great, let’s use one:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">timed</span><span class="p">(</span><span class="s2">"With transaction"</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">db</span> <span class="o">=</span> <span class="n">create_db</span>
  <span class="n">db</span><span class="p">.</span><span class="nf">transaction</span> <span class="k">do</span>
    <span class="n">first_record</span> <span class="o">=</span> <span class="n">values</span><span class="p">.</span><span class="nf">first</span>
    <span class="n">cols</span> <span class="o">=</span> <span class="n">first_record</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">", "</span><span class="p">)</span>
    <span class="n">placeholders</span> <span class="o">=</span> <span class="p">([</span><span class="s2">"?"</span><span class="p">]</span> <span class="o">*</span> <span class="n">first_record</span><span class="p">.</span><span class="nf">length</span><span class="p">).</span><span class="nf">join</span><span class="p">(</span><span class="s2">", "</span><span class="p">)</span>
    <span class="n">sql</span> <span class="o">=</span> <span class="s2">"INSERT INTO metrics (</span><span class="si">#{</span><span class="n">cols</span><span class="si">}</span><span class="s2">) VALUES (</span><span class="si">#{</span><span class="n">placeholders</span><span class="si">}</span><span class="s2">)"</span>
    <span class="n">values</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">cols_to_values</span><span class="o">|</span>
      <span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="n">sql</span><span class="p">,</span> <span class="n">cols_to_values</span><span class="p">.</span><span class="nf">values</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>That gives <code class="language-plaintext highlighter-rouge">With transaction - 1.8898840000038035 seconds</code>. Better, but by far not the improvement we need. Let’s use a prepared statement next:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">timed</span><span class="p">(</span><span class="s2">"With transaction and prepared statement"</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">db</span> <span class="o">=</span> <span class="n">create_db</span>
  <span class="n">first_record</span> <span class="o">=</span> <span class="n">values</span><span class="p">.</span><span class="nf">first</span>
  <span class="n">cols</span> <span class="o">=</span> <span class="n">first_record</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">", "</span><span class="p">)</span>
  <span class="n">placeholders</span> <span class="o">=</span> <span class="p">([</span><span class="s2">"?"</span><span class="p">]</span> <span class="o">*</span> <span class="n">first_record</span><span class="p">.</span><span class="nf">length</span><span class="p">).</span><span class="nf">join</span><span class="p">(</span><span class="s2">", "</span><span class="p">)</span>
  <span class="n">sql_stmt</span> <span class="o">=</span> <span class="s2">"INSERT INTO metrics (</span><span class="si">#{</span><span class="n">cols</span><span class="si">}</span><span class="s2">) VALUES (</span><span class="si">#{</span><span class="n">placeholders</span><span class="si">}</span><span class="s2">)"</span>
  <span class="n">db</span><span class="p">.</span><span class="nf">transaction</span> <span class="k">do</span>
    <span class="n">prepared_stmt</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="nf">prepare</span><span class="p">(</span><span class="n">sql_stmt</span><span class="p">)</span>
    <span class="n">values</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">cols_to_values</span><span class="o">|</span>
      <span class="n">prepared_stmt</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="n">cols_to_values</span><span class="p">.</span><span class="nf">values</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This gives: <code class="language-plaintext highlighter-rouge">With transaction and prepared statement - 0.6456299999845214 seconds</code> - much better. But we can go further. By default SQLite optimizes for durability (at least on my version). Since we are working with a local database and we do not care about a potential crash, we can “downgrade” the durability of the storage engine to get more speed:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">timed</span><span class="p">(</span><span class="s2">"With pragmas, transaction and prepared statement"</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">db</span> <span class="o">=</span> <span class="n">create_db</span>
  <span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="s2">"PRAGMA synchronous = OFF"</span><span class="p">)</span>
  <span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="s2">"PRAGMA journal_mode = OFF"</span><span class="p">)</span>

  <span class="n">first_record</span> <span class="o">=</span> <span class="n">values</span><span class="p">.</span><span class="nf">first</span>
  <span class="n">cols</span> <span class="o">=</span> <span class="n">first_record</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">", "</span><span class="p">)</span>
  <span class="n">placeholders</span> <span class="o">=</span> <span class="p">([</span><span class="s2">"?"</span><span class="p">]</span> <span class="o">*</span> <span class="n">first_record</span><span class="p">.</span><span class="nf">length</span><span class="p">).</span><span class="nf">join</span><span class="p">(</span><span class="s2">", "</span><span class="p">)</span>
  <span class="n">sql_stmt</span> <span class="o">=</span> <span class="s2">"INSERT INTO metrics (</span><span class="si">#{</span><span class="n">cols</span><span class="si">}</span><span class="s2">) VALUES (</span><span class="si">#{</span><span class="n">placeholders</span><span class="si">}</span><span class="s2">)"</span>
  <span class="n">db</span><span class="p">.</span><span class="nf">transaction</span> <span class="k">do</span>
    <span class="n">prepared_stmt</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="nf">prepare</span><span class="p">(</span><span class="n">sql_stmt</span><span class="p">)</span>
    <span class="n">values</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">cols_to_values</span><span class="o">|</span>
      <span class="n">prepared_stmt</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="n">cols_to_values</span><span class="p">.</span><span class="nf">values</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Still better: <code class="language-plaintext highlighter-rouge">With pragmas, transaction and prepared statement - 0.6219140000175685 seconds</code> - this is already a substantial improvement, but we can give the crank another turn.</p>

<h3 id="host-parameter-stuffing">Host parameter stuffing</h3>

<p>How can we make it even faster than that? Well, the <code class="language-plaintext highlighter-rouge">INSERT</code> SQL statement supports multiple tuples in sequence, as long as they have the same cardinality. A bit like so:</p>

<pre><code class="language-SQL">  INSERT INTO metrics (name, value) VALUES ('foo', 1.0), ('bar', 2.0), ('baz', 4.0)
</code></pre>

<p>We can assign our placeholders in the prepared statement and then pass our bound parameters in the end:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="s2">"INSERT INTO metrics (name, value) VALUES (?, ?), (?, ?)"</span><span class="p">,</span> <span class="p">[</span><span class="s2">"foo"</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="s2">"bar"</span><span class="p">,</span> <span class="mf">2.0</span><span class="s2">"])
</span></code></pre></div></div>

<p>But there is a limit - the maximum number of bound variables per SQL statement, varies with the version of SQLite. Sadly, the <code class="language-plaintext highlighter-rouge">sqlite3</code> gem does not support querying for <code class="language-plaintext highlighter-rouge">sqlite3_limit()</code>, but the <a href="https://www.sqlite.org/limits.html">info</a> says:</p>

<blockquote>
  <p>To prevent excessive memory allocations, the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER, which defaults to 999 for SQLite versions prior to 3.32.0 (2020-05-22) or 32766 for SQLite versions after 3.32.0.</p>
</blockquote>

<p>There is, in fact, another way to access that limit - <a href="https://www.sqlite.org/pragma.html#pragfunc">pragma functions</a>, although these exist from a certain version up. You can also use <code class="language-plaintext highlighter-rouge">PRAGMA compile_options</code> for older SQLite builds.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">compile_option</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="nf">get_first_value</span><span class="p">(</span><span class="s2">"SELECT * FROM pragma_compile_options WHERE compile_options LIKE 'MAX_VARIABLE_NUMBER%'"</span><span class="p">)</span>
<span class="n">max_bindvars</span> <span class="o">=</span> <span class="p">(</span><span class="n">compile_option</span><span class="p">.</span><span class="nf">to_s</span><span class="p">[</span><span class="sr">/\d+$/</span><span class="p">]</span> <span class="o">||</span> <span class="mi">999</span><span class="p">).</span><span class="nf">to_i</span>
</code></pre></div></div>

<p>What we need to do is figure out how many of our records we can stuff into a single <code class="language-plaintext highlighter-rouge">INSERT</code> - since we cannot really “split” the records, we always need to insert all values pertaining to a single record in one statement.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">first_record</span> <span class="o">=</span> <span class="n">values</span><span class="p">.</span><span class="nf">first</span>
<span class="n">cardinality</span> <span class="o">=</span> <span class="n">first_record</span><span class="p">.</span><span class="nf">length</span>
<span class="n">records_per_statement</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">max_bindvars</span><span class="p">.</span><span class="nf">divmod</span><span class="p">(</span><span class="n">cardinality</span><span class="p">)</span>
</code></pre></div></div>
<p>This shows us that we can at most stuff <code class="language-plaintext highlighter-rouge">records_per_statement</code> into a single <code class="language-plaintext highlighter-rouge">INSERT</code> (the remainder is not really useful here).
We will use 2 statements, one of which we will prepare - since it is going to be reused. The first one will fit as many records as we can and bind variables for all of them - 999 bindvars or less, depending on the cardinality of our records. The second one will contain enough bindvars to fit the remaining records, and will be used only once - in fact, we do not even need to prepare it.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">timed</span><span class="p">(</span><span class="s2">"With multirow inserts, pragmas, transaction and prepared statement"</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">db</span> <span class="o">=</span> <span class="n">create_db</span>
  <span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="s2">"PRAGMA synchronous = OFF"</span><span class="p">)</span>
  <span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="s2">"PRAGMA journal_mode = OFF"</span><span class="p">)</span>

  <span class="n">first_record</span> <span class="o">=</span> <span class="n">values</span><span class="p">.</span><span class="nf">first</span>

  <span class="c1"># We need to group our records into blocks of at most max_bindvars values</span>
  <span class="n">cardinality</span> <span class="o">=</span> <span class="n">first_record</span><span class="p">.</span><span class="nf">length</span>
  <span class="n">row_placeholder</span> <span class="o">=</span> <span class="s2">"("</span> <span class="o">+</span> <span class="p">([</span><span class="s2">"?"</span><span class="p">]</span> <span class="o">*</span> <span class="n">cardinality</span><span class="p">).</span><span class="nf">join</span><span class="p">(</span><span class="s2">", "</span><span class="p">)</span> <span class="o">+</span> <span class="s2">")"</span> <span class="c1"># =&gt; (?, ?, ?)</span>
  <span class="n">max_bindvars</span> <span class="o">=</span> <span class="mi">999</span>
  <span class="n">max_records_per_statement</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">max_bindvars</span><span class="p">.</span><span class="nf">divmod</span><span class="p">(</span><span class="n">cardinality</span><span class="p">)</span>
  <span class="n">prepared_statement_for_max</span> <span class="o">=</span> <span class="kp">nil</span>
  <span class="n">cols</span> <span class="o">=</span> <span class="n">first_record</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">", "</span><span class="p">)</span>

  <span class="n">db</span><span class="p">.</span><span class="nf">transaction</span> <span class="k">do</span>
    <span class="n">values</span><span class="p">.</span><span class="nf">each_slice</span><span class="p">(</span><span class="n">max_records_per_statement</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">records_subset</span><span class="o">|</span>
      <span class="n">bound_params</span> <span class="o">=</span> <span class="n">records_subset</span><span class="p">.</span><span class="nf">flat_map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:values</span><span class="p">)</span>
      <span class="k">if</span> <span class="n">records_subset</span><span class="p">.</span><span class="nf">length</span> <span class="o">==</span> <span class="n">max_records_per_statement</span>
        <span class="n">prepared_statement_for_max</span> <span class="o">||=</span> <span class="k">begin</span>
          <span class="n">placeholders_for_larger_chunk</span> <span class="o">=</span> <span class="p">([</span><span class="n">row_placeholder</span><span class="p">]</span> <span class="o">*</span> <span class="n">max_records_per_statement</span><span class="p">).</span><span class="nf">join</span><span class="p">(</span><span class="s2">", "</span><span class="p">)</span>
          <span class="n">sql_max</span> <span class="o">=</span> <span class="s2">"INSERT INTO metrics (</span><span class="si">#{</span><span class="n">cols</span><span class="si">}</span><span class="s2">) VALUES </span><span class="si">#{</span><span class="n">placeholders_for_larger_chunk</span><span class="si">}</span><span class="s2">"</span>
          <span class="n">db</span><span class="p">.</span><span class="nf">prepare</span><span class="p">(</span><span class="n">sql_max</span><span class="p">)</span>
        <span class="k">end</span>
        <span class="n">prepared_statement_for_max</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="n">bound_params</span><span class="p">)</span>
      <span class="k">else</span>
        <span class="c1"># This is the last slice which is smaller</span>
        <span class="n">placeholders_for_smaller_chunk</span> <span class="o">=</span> <span class="p">([</span><span class="n">row_placeholder</span><span class="p">]</span> <span class="o">*</span> <span class="n">records_subset</span><span class="p">.</span><span class="nf">length</span><span class="p">).</span><span class="nf">join</span><span class="p">(</span><span class="s2">", "</span><span class="p">)</span>
        <span class="n">sql_rest</span> <span class="o">=</span> <span class="s2">"INSERT INTO metrics (</span><span class="si">#{</span><span class="n">cols</span><span class="si">}</span><span class="s2">) VALUES </span><span class="si">#{</span><span class="n">placeholders_for_smaller_chunk</span><span class="si">}</span><span class="s2">"</span>
        <span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="n">sql_rest</span><span class="p">,</span> <span class="n">bound_params</span><span class="p">)</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Running all of our implementations then gives us:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Naive - 2.7048650000069756 seconds
With transaction - 2.3600640000076964 seconds
With transaction and prepared statement - 0.637083999987226 seconds
With pragmas, transaction and prepared statement - 0.6406159999896772 seconds
With multirow inserts, pragmas, transaction and prepared statement - 0.3141590000013821 seconds
</code></pre></div></div>

<p>We can see that using multirow inserts gives us a 2x speedup. Splendid.</p>

<h3 id="memory-databases-to-disk">Memory databases to disk</h3>

<p>Of course, this is with memory databases - so it is probably very fast because of that. But what if I told you that you can actually serialize a memory DB onto disk very quickly, just using the builtin SQLite functions? A little-known feature of SQLite called <a href="https://sqlite.org/c3ref/backup_finish.html">online backup</a> can be used to prepare your database in memory, do all of the bulk operations – and then write it out onto the filesystem, in a very fast (and consistent) way. The API in the Ruby gem is not pretty - but it is there and it works, and it works well (has been for more than a decade, in fact). Let’s put it to use:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">write_to_disk</span><span class="p">(</span><span class="n">source_db</span><span class="p">,</span> <span class="n">filename</span><span class="p">)</span>
  <span class="n">destination_db</span> <span class="o">=</span> <span class="no">SQLite3</span><span class="o">::</span><span class="no">Database</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span>
  <span class="n">b</span> <span class="o">=</span> <span class="no">SQLite3</span><span class="o">::</span><span class="no">Backup</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">destination_db</span><span class="p">,</span> <span class="s1">'main'</span><span class="p">,</span> <span class="n">source_db</span><span class="p">,</span> <span class="s1">'main'</span><span class="p">)</span>
  <span class="k">begin</span>
    <span class="n">b</span><span class="p">.</span><span class="nf">step</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
  <span class="k">end</span> <span class="k">while</span> <span class="n">b</span><span class="p">.</span><span class="nf">remaining</span> <span class="o">&gt;</span> <span class="mi">0</span>
  <span class="n">b</span><span class="p">.</span><span class="nf">finish</span>
  <span class="n">destination_db</span><span class="p">.</span><span class="nf">close</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Running the code gives us:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Naive - 2.706573000003118 seconds
With a prepared statement - 0.990191999997478 seconds
With transaction and prepared statement - 0.627656000026036 seconds
With pragmas, transaction and prepared statement - 0.6277800000098068 seconds
With multirow inserts, pragmas, transaction and prepared statement - 0.3135960000217892 seconds
</code></pre></div></div>

<p>These timings include serialization to disk using the backup API. And produces a few SQLite3 files of exactly the same size.</p>

<h3 id="comparing-disk-and-memory-performance">Comparing disk and memory performance</h3>

<p>Out of curiosity, I would like to show what kind of performance we can have if we perform the same “accelerated” inserts on a disk DB, with the same 500000 rows:</p>

<p>Disk DBs:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Naive - 474.50496300001396 seconds
With transaction - 2.3882359999988694 seconds
With transaction and prepared statement - 0.7706829999806359 seconds
With pragmas, transaction and prepared statement - 0.7386469999910332 seconds
With multirow inserts, pragmas, transaction and prepared statement - 0.3565039999957662 seconds
</code></pre></div></div>

<p>Memory DBs:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Naive - 2.3065050000150222 seconds
With transaction - 1.8898840000038035 seconds
With transaction and prepared statement - 0.6456299999845214 seconds
With pragmas, transaction and prepared statement - 0.634888000000501 seconds
With multirow inserts, pragmas, transaction and prepared statement - 0.31626500000129454 seconds
</code></pre></div></div>

<p>This is curious: using a DB in RAM only helps is in the most pathological case with our “naive” inserts – but for other cases performance is on par. Aren’t modern SSDs marvelous?</p>

<p>So there you have it: a roughly <strong>x8</strong> speedup for inserts.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[In my work I tend to reach for SQLite more and more. The type of work I find it useful for most these days is quickly amalgamating, dissecting, collecting and analyzing large data sets. As I have outlined in my Euruko talk on scheduling, a key element of the project was writing a simulator. That simulator outputs metrics - lots and lots of metrics, which resemble what our APM solution collects. Looking at those metrics makes it possible to plot, dissect and examine the performance of various job flows. You can, of course, store those metrics in plain Ruby objects and then work with them in memory - there is nothing wrong with that. However, I find using SQL vastly superior. And since the simulator only ever runs on one machine, and every session is unique - SQLite is the perfect tool for collecting metrics. Even if it is not a specialized datastore. One challenge presented itself, though: those metrics get output in very large amounts. Every tick of the simulator can generate thousands of values. Persisting them to SQLite is fast, but with very large amounts that “fast” becomes “not that fast”. I had to go through a number of steps to make these inserts more palatable, which led to a very, very pleasant speed improvement indeed. That seems worth sharing - so strap in and let’s play.]]></summary></entry><entry><title type="html">Joke accounts are a bitter necessity</title><link href="https://blog.julik.nl/2024/04/joke-accounts-are-a-bitter-necessity" rel="alternate" type="text/html" title="Joke accounts are a bitter necessity" /><published>2024-04-20T00:00:00+00:00</published><updated>2024-04-20T00:00:00+00:00</updated><id>https://blog.julik.nl/2024/04/joke-accounts-are-a-bitter-necessity</id><content type="html" xml:base="https://blog.julik.nl/2024/04/joke-accounts-are-a-bitter-necessity"><![CDATA[<p><a href="https://blog.aurynn.com/2017/06/01-joke-accounts-and-the-bofh-are-garbage">Joke Accounts and the BOFH are Garbage</a> by Aurynn Shaw struck a chord with me back in the day. After all, who wants to exclude people? Who wants to make them feel unwelcome? Having survived some amount of difficult working experiences I have changed my mind on this drastically. The joke accounts are a bitter necessity, and I’ll try to explain why.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p><a href="https://en.wikipedia.org/wiki/Satire">Satire</a> is not only an aspect of culture. In many ways, it is a tool of survival, but also a tool of <a href="https://perspectives.ushmm.org/tag/humor-satire">expressing cricicism and relieving tension.</a> The “joke accounts” are a tool of that expression, and an outlet for emotions which do not have an official spot in our modern, Western work culture. When <a href="https://en.wikipedia.org/wiki/Bullshit_Jobs">bullshit jobs</a> run rampant, and every organisation you join starts to resemble some form of a <a href="https://www.amazon.com/Moral-Mazes-World-Corporate-Managers/dp/0199729883">moral maze,</a> satire is one of the few things that can help.</p>

<p>Sardonic laughter can be one of the very few weapons that remain and help keep one’s sanity.</p>

<ul>
  <li>When you spend 70% of your days in meetings, and the rest - in Scrum rituals - no, it is not you, it is a working culture that is wrong for you, and it is likely shit.</li>
  <li>When you find that a glass ceiling has been erected above you in the career path - no, it is not you, this is how modern enterprises work, and it is likely shit.</li>
  <li>When you find that building the frontend code for your application takes north of a few minutes, and MP4 files get embedded into the JavaScript that gets served to clients - it is not you, it is a terrible setup which is a mistake, and it is, indeed, mosty probably, shit.</li>
</ul>

<p>While a <a href="https://hbr.org/2023/05/how-to-build-a-blameless-work-culture">blameless culture</a> is a noble strife, and is an essential stepping stone to building a healthy team, just as any other phenomenon it can overfit. Have too much of blameless culture, and there is no more accountability. Have too much conflict aversion, and no issue is ever important enough to have an argument about. Have too much porcelain, and no behavior will be tolerated - even if it were fully innocent.</p>

<p>Yes, the issue of the devaluation of PHP devs, Javascript devs, CSS devs, designers, Ruby devs, Java devs - any other particular trade devs - is clearly present. But so is the issue of glass ceilings. And so is the issue of overblown Javascript industrial complex, where value extraction gets confounded with simplicity. And so is the fact that many CEOs are utterly incapable of prioritization. The <a href="https://twitter.com/PHP_CEO">PHP CEO Twitter account</a> could as well have been a <code class="language-plaintext highlighter-rouge">JavaCeo</code>, or a <code class="language-plaintext highlighter-rouge">ScalaCeo</code>. It could have been a <code class="language-plaintext highlighter-rouge">ScaleupCeo</code> too. Most of the posts there have nothing to do with PHP - they have to do with a shitty work culture.</p>

<p>When we see retoolings, Glassdoor becoming deanonymized, …. - what <em>is</em> the place to vent then? How can one keep sane when it feels like every behavior - the silliest, the most long-winded, ceremonial, stupid behavior - is normal, acceptable, appropriate?</p>

<p>Living in oppressive societies leaves a certain mark, an imprint. That imprint dictates that if something ridiculous and probably harmful happens, you should either accept it, or admit it is ridiculous and harmful - and likely face consequences. Questioning the status quo is a danger to well-being. The majority then becomes “silent” exactly because, facing the prospect of being punished, they either “yield to authority” or try to mask their resentment with “there are probably valid reasons for this”. This is my fundamental objection to the Chesterton’s Fence too - it does not allow investigation into whether the fence has any merit, it dictates the fence just be left exactly where it is. The fear of getting reprimanded generates this tacit acceptance. One step further - and you already think “it is probably me who is insane”. One step further still - and we get to fatigue, disenchantment, and finally: burnout.</p>

<p>Fighting this is what satire accounts are about. They are not about “othering female frontend developers”. They are not about “creating an in-group”. They are about the desperate need to be heard when you feel like everything around you has turned into a circus performance, and you yourself - against your will - have been made a clown.</p>

<p>Plenty of that at <a href="https://www.reddit.com/r/devhumor/">r/devhumor.</a></p>

<p>Let’s make no mistake here. We <a href="https://grumpy.website/">are routinely building bad UIs</a> and there are terrible team dynamics and incentives <em>directly</em> fueled by how our tech industry is messed up. We absolutely <a href="https://devops.com/agile-scrum-is-a-failure-heres-why/">are applying a distorted and ritualised practice.</a> We absolutely <a href="https://www.forbes.com/sites/dwightsilverman/2021/05/17/the-web-can-be-very-annoying-heres-how-to-fix-that/">are putting too many tracking and popups on websites.</a> If we disallow satire, if we disallow exposure for those things - yes, we are being gentle on people who have no other choice than to implement all of this, who have no other choice than to “get in line”. And we are, as an industry, more inclusive, because nothing would ever be labeled “laughable”. But at the same time, we are then making a tacit admission that all of this crap just “came out of nowhere”. It just, sort of, “happened”, you know? UIs that are impossible to use, sites, where the content is concealed for 70 percent by ads, Scrum daily standups which drag on for hours and become a glorified status update, engineers and designers getting ignored with any kind of product input, layoffs, AI supplanting humans…</p>

<p>And while there is no “single person” to blame for all of that, there are certainly directions of thought - and institutions - and yes, people – that absolutely are accountable for where we are. Some of people, specifically, are considered exemplar in the field. And while “voting with your feet” (and going to work on <a href="https://joinmastodon.org/careers#open-positions">something like Mastodon</a>, provided you have amassed enough <a href="https://www.straight.com/guides/finance/fuck-you-money/">FU money</a> and have no caretaker duties, children or mortgage) is maybe an option, the first thing you likely need is waking up from the nightmare. Because any oppressive structure is designed to condition you to believe that <em>it is you who is the problem</em> – as the survival of that system is contingent on you believing it. <a href="https://www.youtube.com/watch?v=sjoad6gcRzs">Wake up, Neo!</a> It’s not you. Or, at the very least, it’s not you as often as you have been made to believe.</p>

<p>BOFH is a curious case here.</p>

<blockquote>
  <p>I carried this forward as the wish fulfilment of being taught to think I was better than users, smarter and more capable</p>
</blockquote>

<p>While it is certainly humorous and makes heaps of fun at helpless “users” whose mailbox is getting deleted story after story after story, it seems odd that a fairly obvious point eluded Aurynn’s attention. BOFH is a satire upon itself, and the culture it makes a joke of is the culture of petty systems administrators who do evil deeds just for their own pleasure. Who forbid essential things from happening just because they like to feel the power bestowed upon them. Who reduce usage quotas “just so that these lame users don’t relax too much”. This is what BOFH is about – way more than being directed at “users” it is directed at the ego-centric culture of systems administrators. I have been on both ends of that stick, and the better admins I worked with and have as friends support this claim. BOFH is satire about “how we suck”, not about “how those <em>others</em> suck”. And should you wander, it can be a good wakeup call. It is that culture which birthed the more dysfunctional versions of “infra” and “devops” teams in late 2010-s. Of course – not all of them. But those that are hard to collaborate with stem from exactly the type of culture BOFH was describing. And scolding.</p>

<p>There is a concept of <a href="https://www.psychologytoday.com/us/blog/digital-altruism/202003/the-antifragile-mindset">anti-fragility</a> which has been appropriated by the more right-wing spectrum, but its message is something I would like to subscribe by:</p>

<blockquote>
  <p>Antifragility is beyond resilience or robustness. The resilient resists shocks and stays the same; the antifragile gets better.</p>
</blockquote>

<p>Satire is essential for providing shocks that make us all better. But we ought to pick to be antifragile. I find it ironic that exactly the groups which could benefit the most from open-heartedly laughing about the mess that is our field of work, instead choose to suppress it – and to accuse its creators.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Joke Accounts and the BOFH are Garbage by Aurynn Shaw struck a chord with me back in the day. After all, who wants to exclude people? Who wants to make them feel unwelcome? Having survived some amount of difficult working experiences I have changed my mind on this drastically. The joke accounts are a bitter necessity, and I’ll try to explain why.]]></summary></entry><entry><title type="html">Reviving zip_tricks as zip_kit</title><link href="https://blog.julik.nl/2024/04/reviving-zip-tricks" rel="alternate" type="text/html" title="Reviving zip_tricks as zip_kit" /><published>2024-04-09T00:00:00+00:00</published><updated>2024-04-09T00:00:00+00:00</updated><id>https://blog.julik.nl/2024/04/reviving-zip-tricks</id><content type="html" xml:base="https://blog.julik.nl/2024/04/reviving-zip-tricks"><![CDATA[<p>Well-made software has a lifetime, and the lifetime is finite. However, sometimes software becomes neglected way before its lifetime comes to an end. Not obsoleted, not replaced - just.. neglected. Recently I have decided to resurrect one such piece of software.</p>

<p>See, <a href="https://github.com/WeTransfer/zip_tricks">zip_tricks</a> holds a special place in my heart. It was quite difficult to make, tricky, but exceptionally rewarding. It also went through a number of iterations, and working on it taught me a great lot. How short methods are not always a good thing. How it is important to provide defaults. How over-reliance on teensy-tinesy-objects can make software hard to read and understand (in case of Rubyzip). And how open source might work in a corporate setting.</p>

<p>What follows is the story of how zip_tricks became <a href="https://github.com/julik/zip_kit">zip_kit</a> and what I have learned along the way.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>There was a time in my life when I was trying to do a startup. As a matter of fact, I tried to do the same startup <em>twice</em> and also <em>failed twice</em> at that – if I ever get over my anxieties and regrets I might write a story about that too. Anyway, during that <em>second</em> attempt at it, I wanted to implement some functionality so that you could download a pack of files all-packaged into a neat ZIP.</p>

<p>And since I built a gem <em>exactly for that</em> - what could be more obvious than to use it? So we did. Except that… it didn’t stream. I have put resolving this into our backlog, as there was just… <em>so</em> much other stuff to do. A couple of years after, <a href="https://cheddar.me">I joined Cheddar</a> - and again, found that we had a few report-downloading endpoints which were generating ZIPs. And, oh-horror, they were using Rubyzip and they were generating tempfiles, and it was slow and unwieldy. How could I walk past this and leave this untouched? Again: some code replaced, some tweaks made… and the thing is not streaming. It just wouldn’t! And I could not really pinpoint why.</p>

<p>This led me to investigate what really went wrong, and where. But I didn’t like the idea that I have built a library, and I know it is broken, and I would make a fix but would not be able to share it. So I went over to the zip_tricks repo on Github and found that… no updates to it have taken place, since the time I left. This led me to ponder…</p>

<h2 id="story-of-zip_tricks-and-zip_kit">Story of zip_tricks (and zip_kit)</h2>

<p>zip_tricks got started as a set of hacks on top of Rubyzip, which I basically copied from <a href="https://github.com/fringd/zipline">zipline.</a> We wanted to assemble ZIPs on the fly, and Ruby did not, at the time, have <em>any</em> libraries which would do that. There was a gem which did work, to an extent, but it did not support Zip64 (large files), which we absolutely required to work. Once it became clear that the very fabric of Rubyzip does not allow us to build a streaming solution, zip_tricks (which used to be just an internal library) has rejected Rubyzip as a dependency, and started providing a ZIP writer of its own. Later on we needed to do a large migration, which entailed “unpacking” the existing ZIP transfers into their constituent files - so that they could afterwards get re-assembled on the fly. The number of those ZIPs - which were pre-generated for every transfer - neared about a dozen million, if note more. Most of them - very large. Some of them - having corrupted structure. It would simply not be possible to “expand” those ZIPs by downloading them onto the EC2 instances first. The filesystem on EC2 is severely throttled, and some of the files would simply not fit on the EC2’ instances. So the second application of zip_tricks was to permit this “lazy unpacking” of ZIP files which could be done directly via HTTP.</p>

<p>Then we have overhauled our download servers and implemented resumable ZIP downloads with dynamic addressing. To my knowledge, this is something even Google Drive does not support <em>to this day</em> - remember the “Preparing ZIP” popover? How it works is described in <a href="https://github.com/WeTransfer/zip_tricks/issues/109">this GH issue</a> and <a href="https://betterprogramming.pub/how-to-implement-multi-file-downloads-in-ruby-web-apps-a7a6090f3ab5?gi=317dfd2d263a">this blog article,</a> and it worked perfectly. This was actually the main use-case for zip_tricks once “the big migration” was completed. We would generate a “manifest” containing URLs for edge-includes and some ZIP file bytes for the central directory and local headers, and our download servers would stream those manifests out, substituting actual files from S3.</p>

<p>After I have left WeTransfer, zip_tricks became abandoned. For a number of reasons, really. The strong Ruby clique at WT has mostly disintegrated with myself leaving for Cheddar, Fabio leaving for Booking - and a few other folks being gone too. To much of my chagrin, TypeScript and Go have won at WT without much hope for recourse for folks still writing Ruby, even though there was a lot of potential to realize. As a matter of policy, myself and others who left lost committer rights to all the OSS repositories they were maintaining.</p>

<p>That was unfortunate. I had no grudges and was, in principle, open to maintaining stuff I used to maintain indefinitely - particularly the things that I was using myself. It was not to be, however. The few PRs I have opened stayed open for a long while, and there wasn’t a single release of zip_tricks since I have left.</p>

<p>And zip_tricks holds a special place in my heart. I really wanted to fix it, to “own” it again (“ownership” in the context of teamwork and organisations in general is something I still haven’t been able to wrap my head around). Since taking the software over turned out not to be an option, I decided to take the “measure 0” and fork zip_tricks under a different name.</p>

<p>And so came <code class="language-plaintext highlighter-rouge">zip_kit</code>.</p>

<p>A few changes came with that.</p>

<h2 id="preventing-corporate-open-source-abandonware">Preventing corporate open-source abandonware</h2>

<p>What happened as part of WT’s open source policy actually led me to adopt a completely different approach at Cheddar. Everybody who left the company on friendly terms stays a contributor to the open-source libs. We hardly had a moment when this was objectionable.</p>

<p>It might just so be, that the nature of open-source - which makes forking possible - has made ressurecting zip_kit viable. And I am very grateful for that.</p>

<p>And we are not on the ball with something - maintainership-post-leave. Quite a few corporate OSS projects are <em>not</em> actually corporate-backed. They are backed insofar as the maintainer gets paid to work on that software within their usual work responsibilities - because it is needed to the business, but the fact that the software is open-source is often an afterthought. The company does get their profit by having a better public image (“we are supporting open-source”), the maintainer gets profit by having creative control (“look how I am releasing software without being subjected to the feature-factory-waterfall-scrumgile-theater”). But while a company might not help the maintainer with any marketing or publicity - something that is very much necessary for an OSS project to succeed - it will often claim copyright and control over that OSS. This means that once the maintainer leaves the company, it is not very likely they will be able to “carry” the software with them and keep maintaining it. “Why is that weird person still part of our Github organization and <a href="https://github.com/pricing">costing us 4 bucks a month?</a>” “-Oh, that’s Jake and they used to work here 3 years ago and they wrote libfoo, which we are not even using anymore, but they still need access”.</p>

<p>I would posit that 99% of what we consider “corporate-backed OSS” at this point is something like libfoo, and we – as an industry – do not necessarily have a good story in place for assuring maintenance. That provided a maintainer is willing to step up and do it, which might not even be the case. People change, their incentives and priorities change, people become caregivers, people burn out, people <a href="https://sass-lang.com/documentation/cli/ruby-sass/">switch stacks</a> just because they feel like it, people… <a href="https://en.wikipedia.org/wiki/Pieter_Hintjens">get ill and die, too.</a></p>

<p>What the OSS policy has led to in case of zip_tricks was that even though I was willing to continue maintaining and have been sending PRs, they went unmerged for months.</p>

<h2 id="a-good-open-source-policy-for-your-org-might-be-no-policy">A good open source policy for your org might be “no policy”</h2>

<p>Back at WT, at one point one of the team members has raised a point that “we need an open-source policy”. In retrospect, this was a premature call. What came out of that was:</p>

<ul>
  <li>We needed a CLA policy. That was probably a good call, but people were not allowed to implement automation for this - and so it didn’t come to pass.</li>
  <li>Managing GH repos would be in purview of one of the teams (which never published its own open-source software). People authoring OSS would not be permitted to allow external contributors</li>
  <li>Every time an OSS library needed, say, cloud resources, there would be a negotiation process to get them. Since a lot of the sofware had to do with the cloud (AWS for us), this hampered the flexibility considerably as well</li>
  <li>The “spicy question” of “what to do if a contributor no longer works here” was left unanswered</li>
</ul>

<p>Most importantly - it created too much process, and it created that process too soon. In retrospect, I should have resisted more on this, and reduce that policy to an absolute minimum. The triangle of <a href="https://www.cambridge.org/core/books/algorithms-and-autonomy/autonomy-agency-and-responsibility/BA4D809382F63A0DB4F9549EE9E99641">agency, autonomy, responsibility</a> was not shaped well there.</p>

<p>Having seen this first-hand, I would say that the only thing that truly makes sense is a good CLA process. A proper provision with a CLA would be that the contributors do not object to you changing the licensing terms on the open-source software you are producing.</p>

<p>And that’s it. No - really - that’s it. For 99% of the joints out there, this will be more than adequate.</p>

<h2 id="forking-as-last-resort">Forking as last resort</h2>

<p>There was, however, some possibility for a revival. Older versions of zip_tricks did not carry the Hippocratic license, and while the name could be considered copyrighted, the actual <em>code</em> could not. And so the decision came about fairly organically: fork the library based on the MIT-licensed branch (which we did keep in place, to not pull the rug from under people who could not permit themselves to use a Hippocratic-licensed library), backport useful changes (most of them were mine anyway), and release it under a different name. And that’s how zip_kit came to be.</p>

<p>The funny part here is that a lot of the “hardliner” provisions of “true” open-source (forking is always an option, a license cannot be turned into a more restrictive license willy-nilly, code can’t be copyrighted…) actually permitted the library to survive.</p>

<h2 id="bidding-farewell-to-the-hippocratic-license">Bidding farewell to the Hippocratic license</h2>

<p>When WT went all-in on the Hippocratic license, it seemed to be a good experiment. I, myself, was mildly curious to try it - who doesn’t want to “do the good thing” and doesn’t want to “hold back those, who do bad things”. Yet: while the emotional message of the Hippocratic license is just, I no longer believe it to be the right call. The problem with the “do no harm” licenses is that it imposes responsibility which is the hardest to enforce when one is in the business of <em>tool making</em> - the responsibility to forbid certain people (or parties) from using your tools. There are instruments of comparable purpose, such as economic sanctions, but any instrument of that nature is as good as it is enforceable. The Hippocratic license is not enforceable for an even moderately-sized company, and much less so - for an individual. Moreover, most folks writing Ruby applications are creating commercial solutions. A commercial solution implies usage by parties you may not even know. It might be used by a company that acquires yours. It might be used by a contractor of a company you have sold to. A company you have sold to may have an “evil” other company amongst its hundreds (or thousands) of clients.</p>

<p>In practice, what the Hippocratic license meant for the software I have built at WT, was this:</p>

<ul>
  <li>Most joints would just avoid using that software - exactly because the license imposed liabilities on them they would not be able to hold themselves accountable for managing, and sometimes would not even bother to understand</li>
  <li>zip_tricks did not become a viable alternative for rubyzip partially because its license was not liberal</li>
  <li>These things were going “to market” in an already minuscule ecosystem (the Ruby/Rails ecosystem), where - even if they had a chance of being popular - they would always be dangerous goods.</li>
</ul>

<p>And have no doubt about it - the OSS software market <em>is</em> a market, only the currency there is the expertise and reputation of the author. While you can’t necessarily “sell” OSS software as a product - although some are trying - you absolutely <em>do</em> market yourself as the maker. Make your bets right and you can count on more interesting gigs, better employment opportunities, and other perks that a lot of… ahem… <em>exposure</em> can get you. Of course, in OSS a lot can get done (and does get done) out of altruistic motives. But pretending we don’t want our OSS software to be popular would be… hypocritical.</p>

<p>The more practical “fallout” of the license was this:</p>

<ul>
  <li>The first dependent library, <a href="https://github.com/fringd/zipline">zipline</a> had to accommodate changes to allow older zip_tricks versions to be used if people are not OK with using the Hippocratic license. Those older versions did not include important bug fixes and tweaks, and code had to be backported into zipline to replicate changes done to zip_tricks itself just to support that capability</li>
  <li>The second dependent library, <a href="https://github.com/felixbuenemann/xlsxtream">xslxstream</a> had to carry an alternative ZIP writer adapter which would allow people to keep using Rubyzip if they do not agree to the license</li>
  <li>The third dependent library… never happened. Plenty of libraries use Rubyzip, but with a restrictive license in tow it was impossible for zip_tricks to sway them over. I don’t have quantitative evidence, but I do have a strong conviction that the fact that zip_tricks remained unpopular was - for the most part - the Hippocratic license it started to carry.</li>
</ul>

<p>Knowing that the Hippocratic license was not working, I never moved any of my own libraries to it - but at the end of my tenure I also have decided not to release libraries via WT, for that reason among others (the other was that managing the Github configuration became a huge nuisance).</p>

<p>With the decision to fork, I was facing a bit of a conundrum. How would it look if I were to take the code that <em>others wrote</em> - assumung we were committed to the Hippocratic license - and remerge it under the MIT license zip_tricks had previously? This is actually something that can be scoffed at. I am, after all, doing something contributors likely haven’t signed up for - making their contributions available to all those Evil Enterprises and Oppressive Government Agencies, right?</p>

<p>In the end the following solution presented itself: I made most of the contributions to zip_tricks. There were a few other contributors, and one of them stood out. If I were to contact them all and wait for their permission, forking under a more permissive license would have taken a very long time indeed. Instead, I have decided to go about it like this:</p>

<ul>
  <li>I would contact the most prolific contributors only, and ask them whether they have objections to the license change</li>
  <li>I would allow anyone’s code to be backed out if they did not agree to the licensing policy.</li>
</ul>

<p>And so it went. And I’ll mention here: if, by any chance, you have contributed to zip_tricks and you object to the license change in zip_kit, please do the following</p>

<ul>
  <li>Contact me (the contact details are on the site as well as in the zip_kit repo) and ask me to back out your contributions, pointing to them specifically</li>
  <li>Continue using zip_tricks instead of zip_kit. You are on your own though regarding upstreaming fixes.</li>
</ul>

<p>With the above, I would now <em>not</em> recommend the MIT-Hippocratic license for new projects. Or rather: I would not recommend it if you value adoption of your software. It is a great idea, but I can’t see it work well.</p>

<h2 id="of-rack-and-streaming">Of Rack and streaming</h2>

<p>Next step was to figure out why streaming was broken. Remember the issue I started the article with? Well, it turned out that it was quite a peculiar thing. See, <code class="language-plaintext highlighter-rouge">zipline</code> did not have that problem - but zip_tricks did! So there was something in there that was causing buffering, and consequently - made zip_tricks useless for exactly the purpose it was made for.</p>

<p>Once I started digging, I was surprised to find that I was not the only one who started to experience buffering. The issue turned out to be <a href="https://github.com/rack/rack/blob/main/lib/rack/etag.rb">the Rack::ETag</a> middleware. This middleware started getting included in the default Rails middleware stack, and what it does is… strange. It computes a “weak” ETag if the underlying Rack app did not compute one, and if the response may be cached. I am, personally, not a fan of solutions like this. It is not wise to buffer (or to iterate over) responses of arbitrary size.</p>

<p>For example, while <a href="https://github.com/julik/idempo">idempo</a> does buffer and cache responses, it does so very carefully. It tries to buffer only in a few, very specific cases: when the response can be sized upfront, when the response already <em>is</em> sized (by a supplied <code class="language-plaintext highlighter-rouge">Content-Length</code> HTTP header), and when that size is below a certain threshold. <code class="language-plaintext highlighter-rouge">Rack::ETag</code>, however, goes about it in a much less gentle manner. Any response which can be cached (or does not have a <code class="language-plaintext highlighter-rouge">Last-Modified</code> or <code class="language-plaintext highlighter-rouge">ETag</code> headers) is going to be checksummed in full before serving starts. The absence of those two headers is what actually caused zip_tricks not to stream inside Rails. This reminded me that while Rack is (in my view) a very decent interface, its semantics are sometimes hard to nail down - and all it takes is one misbehaved middleware to make the whole thing buffer.</p>

<p>It gets even more interesting though. It turned out that in a standard Rack package you would have not <em>one,</em> but <em>two</em> pieces of middleware which could buffer. The other offender is <code class="language-plaintext highlighter-rouge">Rack::ContentLength</code>. Now, Rails does not include <code class="language-plaintext highlighter-rouge">ContentLength</code> - and for a good reason, because it is just not a very well-designed piece of kit, sadly. But there is a fairly common piece of infrastructure which <em>does</em> add the <code class="language-plaintext highlighter-rouge">ContentLength</code> middleware, and it is a fairly unexpected one - the <code class="language-plaintext highlighter-rouge">rackup</code> binary. I tend to use <code class="language-plaintext highlighter-rouge">rackup</code> as the default “serve-this-app-via-Rack” command, and to my surprise <code class="language-plaintext highlighter-rouge">rackup</code> does forcibly add <code class="language-plaintext highlighter-rouge">ContentLength</code> even though nobody asks it to! Luckily this has been removed in recent <code class="language-plaintext highlighter-rouge">rackup</code> versions (moreover, <code class="language-plaintext highlighter-rouge">rackup</code> has become a separate library at this point).</p>

<p>So, what’s the verdict? Well, to fix streaming in (now-renamed) zip_kit, I had to do two things.</p>

<ul>
  <li>For the <code class="language-plaintext highlighter-rouge">Rack::ETag</code> fix, the ZIP archive had to be output with headers containing either a recent <code class="language-plaintext highlighter-rouge">Last-Modified</code> or <code class="language-plaintext highlighter-rouge">ETag</code> header. This was relatively easy to fix.</li>
  <li><code class="language-plaintext highlighter-rouge">Rack::ContentLength</code> is a harder problem. Since Rack is a “diamond dependency” for so many pieces of software (including Rails itself), and can’t thus be easily updated, I needed a way to “switch” <code class="language-plaintext highlighter-rouge">Rack::ContentLength</code> off. There is no “trigger header” implemented, so the only way to do this in a “legitimate” fashion was to pre-apply the <code class="language-plaintext highlighter-rouge">chunked</code> transfer-encoding. That was also fairly easy, given that a middleware for this encoding used to be a part of Rack all the way along, and got even copied into recent Rails.</li>
</ul>

<p>I did get a recommendation from Samuel (of <a href="https://github.com/socketry/async">async</a> fame) to not encode in <code class="language-plaintext highlighter-rouge">chunked</code> forcibly, but to leave it to the webserver. However, given that the <code class="language-plaintext highlighter-rouge">Rack::ContentLength</code> may get introduced into the stack without the user’s consent/knowledge, it was wise to make this an option which can be enabled explicitly. Just for “those” cases.</p>

<p>Once those were fixed, we were back in business - zip_kit was streaming from Rails again. Without using ActionController::Live, too!</p>

<h2 id="maybe-you-should-try-it">Maybe you should try it</h2>

<p>This has turned into a long rant - but this is about stuff that matters deeply to me. It is considered unlealthy to have a meaningful connection to your work in sofware, but despite all of my attempts I just can’t seem to let go of it. Maybe you should give <a href="https://github.com/julik/zip_kit">zip_kit</a> a whirl.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Well-made software has a lifetime, and the lifetime is finite. However, sometimes software becomes neglected way before its lifetime comes to an end. Not obsoleted, not replaced - just.. neglected. Recently I have decided to resurrect one such piece of software. See, zip_tricks holds a special place in my heart. It was quite difficult to make, tricky, but exceptionally rewarding. It also went through a number of iterations, and working on it taught me a great lot. How short methods are not always a good thing. How it is important to provide defaults. How over-reliance on teensy-tinesy-objects can make software hard to read and understand (in case of Rubyzip). And how open source might work in a corporate setting. What follows is the story of how zip_tricks became zip_kit and what I have learned along the way.]]></summary></entry><entry><title type="html">Exploring batch caching of trees</title><link href="https://blog.julik.nl/2024/04/batch-caching-of-trees" rel="alternate" type="text/html" title="Exploring batch caching of trees" /><published>2024-04-02T00:00:00+00:00</published><updated>2024-04-02T00:00:00+00:00</updated><id>https://blog.julik.nl/2024/04/batch-caching-of-trees</id><content type="html" xml:base="https://blog.julik.nl/2024/04/batch-caching-of-trees"><![CDATA[<p>From my other posts it might seem that I am a bit of a React hater - not at all. React and related frameworks have introduced a very powerful concept into the web development field - the concept of materialised trees. In fact, we have been dealing with those in Rails for years now as well. Standard Rails rendering has been a cornerstone of dozens of applications to date, and continues to be so. But once you see “trees everywhere” it is hard not to think about optimising using trees. So let me share an idea I’ve had recently which might as well be very neat.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>When we look at our React components, we will see it output a tree of rendered components (how exactly it “outputs” is not relevant for the conversation). Let’s look at a small tree for a hypothetical blog, just like the one you are reading now. We have a number of posts, and every post has a header and a few paragraphs of copy. A post also has a collection of Comments attached:</p>

<p><img src="/assets/2024-04-01-batch-caching-of-trees/tree.png" alt="Basic tree of components for a blog" /></p>

<p>The same can be said of Rails templates, really:</p>

<p><img src="/assets/2024-04-01-batch-caching-of-trees/tree-2.png" alt="Tree of nested Rails partials" /></p>

<p>Granted, some of these can be actual chunks of HTML that get output - for example, a <code class="language-plaintext highlighter-rouge">_header.erb</code> could be just a chunk of bytes with <code class="language-plaintext highlighter-rouge">&lt;h2&gt;My amazing post&lt;/h2&gt;</code> inside. But the important point which we are going to explore is that it is still a <em>tree</em> of some description.</p>

<p>The tree need not be static. Both with React and with Rails templates the output of the tree may change depending on the logic inside the tree nodes themselves. For example:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;%=</span> <span class="n">render</span> <span class="ss">partial: </span><span class="s2">"admin_actions"</span> <span class="k">if</span> <span class="n">current_user</span><span class="p">.</span><span class="nf">admin?</span> <span class="cp">%&gt;</span>
</code></pre></div></div>

<p>is a very basic piece of conditional logic embedded into the template itself. If our <code class="language-plaintext highlighter-rouge">current_user</code> is <code class="language-plaintext highlighter-rouge">admin?</code> there is an extra node added to the tree for their admin actions. Conditional rendering of React components would be doing approximately the same thing.</p>

<h2 id="a-tree-by-the-waterfall">A tree by the waterfall</h2>

<p>What becomes important - and is a focus of a lot of attention in the JS community now - is <em>data loading.</em> Same applies to us in Rails-land. Here is some data we need for displaying our blog:</p>

<p><img src="/assets/2024-04-01-batch-caching-of-trees/tree-3.png" alt="Loading IDs for tree nodes" /></p>

<p>And any app I’ve seen of any measurable size would do these nested views, and would come to the problem of “N+1 queries”. Combating those is a viable - and necessary - thing to do, but another approach has been very popular with Rails for a decade now, and it is called “Russian doll caching”. It relies on “things” in the application having a <code class="language-plaintext highlighter-rouge">#cache_key</code> they can be reliably identified with, and those cache keys can be used to cache <em>nested</em> pieces of output. Branches, really:</p>

<p><img src="/assets/2024-04-01-batch-caching-of-trees/separate-gets.png" alt="Russian doll caching with branches overlay" /></p>

<p>When we render a view, we follow the following logic for this tree:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">output</span> <span class="o">=</span> <span class="n">cache</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">site</span><span class="p">.</span><span class="nf">cache_key</span><span class="p">)</span>
  <span class="k">return</span> <span class="n">output</span>
<span class="k">else</span>
  <span class="n">render</span><span class="p">(</span><span class="ss">partial: </span><span class="s2">"site"</span><span class="p">,</span> <span class="ss">locals: </span><span class="p">{</span><span class="n">site</span><span class="p">:})</span>
<span class="k">end</span>
</code></pre></div></div>

<p>and inside of the <code class="language-plaintext highlighter-rouge">_site.erb</code> partial, we will do:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">site</span><span class="p">.</span><span class="nf">posts</span><span class="p">.</span><span class="nf">published</span><span class="p">.</span><span class="nf">most_recent_first</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">post</span><span class="o">|</span>
  <span class="k">if</span> <span class="n">output</span> <span class="o">=</span> <span class="n">cache</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">post</span><span class="p">.</span><span class="nf">cache_key</span><span class="p">)</span>
    <span class="n">buffer</span> <span class="o">&lt;&lt;</span> <span class="n">output</span>
  <span class="k">else</span>
    <span class="n">buffer</span> <span class="o">&lt;&lt;</span> <span class="n">render</span><span class="p">(</span><span class="ss">partial: </span><span class="s2">"site"</span><span class="p">,</span> <span class="ss">locals: </span><span class="p">{</span><span class="n">site</span><span class="p">:})</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>and the same for <code class="language-plaintext highlighter-rouge">_comment.erb</code></p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">post</span><span class="p">.</span><span class="nf">comments</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">comment</span><span class="o">|</span>
  <span class="k">if</span> <span class="n">output</span> <span class="o">=</span> <span class="n">cache</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">comment</span><span class="p">.</span><span class="nf">cache_key</span><span class="p">)</span>
    <span class="n">buffer</span> <span class="o">&lt;&lt;</span> <span class="n">output</span>
  <span class="k">else</span>
    <span class="n">buffer</span> <span class="o">&lt;&lt;</span> <span class="n">render</span><span class="p">(</span><span class="ss">partial: </span><span class="s2">"comment"</span><span class="p">,</span> <span class="ss">locals: </span><span class="p">{</span><span class="n">comment</span><span class="p">:})</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Imagine all of our interacting partials (<code class="language-plaintext highlighter-rouge">_site.erb</code>, <code class="language-plaintext highlighter-rouge">_post.erb</code> and <code class="language-plaintext highlighter-rouge">_comment.erb</code>) can be cached using a cache key of their respective model. If we do caching for every one of them, rendering our blog with all cache misses is going to be <code class="language-plaintext highlighter-rouge">1 + 1 + 3 = 5</code>. Every cache miss is going to be a roundtrip to a cache store. Let’s imagine it costs us 5 milliseconds, and we have a <code class="language-plaintext highlighter-rouge">Site</code> with 20 posts, and every post has at least 9 comments <code class="language-plaintext highlighter-rouge">1 + (20 * 9) = 181</code> This is already somewhat sizeable, and gives us the following prediction for end-user latency (just for our server rendering part - the client hasn’t even started receiving data at this stage) - <code class="language-plaintext highlighter-rouge">(1 + (20 * 9)) * 5 = 905</code>, close to a whole second just to get the cache misses. This problem is also known as <em>waterfalling</em> in the JS circles - remember how miserable it makes you when your JIRA screen painstakingly pops up pane after pane? This is the same, but worse - while the server is busy doing this work, the client will hardly see any progress. Loaded data begets more loaded data, until the entire tree has been resolved.</p>

<p>What this actually does in terms of tree traversal is called <em>depth first traversal</em> - it goes to a node which hasn’t been rendered yet, and tries an immediate cache get for that particular node. If there is a cache hit - it takes the cached data and substitutes it into the output. If there isn’t - the node gets rendered by running its template. During rendering, the node in turn traverses “into” itself, and tries to retrieve its children from the cache, one by one.</p>

<h2 id="cheaper-in-bulk">Cheaper in bulk</h2>

<p>For this, we can do some optimisations. Imagine our comment rendering does look like this instead:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">render</span><span class="p">(</span><span class="ss">collection: </span><span class="n">post</span><span class="p">.</span><span class="nf">comments</span><span class="p">,</span> <span class="ss">partial: </span><span class="s2">"comment"</span><span class="p">,</span> <span class="ss">cached: </span><span class="kp">true</span><span class="p">)</span>
</code></pre></div></div>

<p>This enables an optimisation which is built in into recent Rails, where a <code class="language-plaintext highlighter-rouge">CollectionRenderer</code>, where the renderer will first do a <code class="language-plaintext highlighter-rouge">Rails.cache.read_multi(...)</code> on all of the <code class="language-plaintext highlighter-rouge">cache_key</code> values for your <code class="language-plaintext highlighter-rouge">Comment</code> models. It will then only run the rendering via the <code class="language-plaintext highlighter-rouge">_comment.erb</code> partial for the <code class="language-plaintext highlighter-rouge">Comment</code> models for which no cache has been found.</p>

<p>The number of cache roundtrips with all misses is going to be <code class="language-plaintext highlighter-rouge">1 + (20 * 2) = 41</code>, way less than our previous approach where we had <code class="language-plaintext highlighter-rouge">1 + (20 * 9) = 181</code>. In addition, <code class="language-plaintext highlighter-rouge">CollectionRenderer</code> is going to <code class="language-plaintext highlighter-rouge">write_multi</code> to fold the saving of the cached data into one roundtrip per collection rendered.</p>

<p>In terms of tree traversal, this is a combination of <em>depth-first traversal</em> (traversing each <code class="language-plaintext highlighter-rouge">post</code> partial of the <code class="language-plaintext highlighter-rouge">blog</code> template) and <em>breadth-first</em> traversal (rendering a collection of <code class="language-plaintext highlighter-rouge">Comments</code>).</p>

<p>But what if we could do better?</p>

<h2 id="the-broader-picture">The broader picture</h2>

<p>The <code class="language-plaintext highlighter-rouge">CollectionRenderer</code> makes use of a great property of collections - it is known which template (partial) has to be executed for every collection member, and the collection members are known ahead of time. Stack those collection renderers, and you could get some good performance increases. Walking a tree would look like this then:</p>

<p><img src="/assets/2024-04-01-batch-caching-of-trees/stepped-fetches-collections.png" alt="Stepped Fetches Collections" /></p>

<p>Green nodes are what we managed to retrieve from the cache, red ones are cache misses. Every dashed rectangle is a cache fetch. It all seems fine, but there is a catch. Imagine we have a tree like this:</p>

<p><img src="/assets/2024-04-01-batch-caching-of-trees/wide-tree.png" alt="Wide Tree" /></p>

<p>Here, the number of roundtrips is going to grow quickly, because even though we are doing a breadth-first traversal of a particular collection, we still can have multiple collections at the same level - and therefore, multiple cache roundtrips. For example, if we have 3 <code class="language-plaintext highlighter-rouge">_post.erb</code> partials, even with a collection renderer first there will be a <code class="language-plaintext highlighter-rouge">read_multi</code> for the <code class="language-plaintext highlighter-rouge">comments</code> of the first <code class="language-plaintext highlighter-rouge">Post</code>, then another for the <code class="language-plaintext highlighter-rouge">comments</code> of the second - and so forth.</p>

<p>In this example, every dashed box is a cache fetch (and a potential cache miss). We have 1 roundtrip at the root of the tree, then 3 roundtrips ad the second level, and 4 roundtrips one level further down. This can quickly escalate into a waterfall of cache misses. Imagine one such miss taking 5ms, and with a tree with an “unlucky” configuration you will quickly waterfall into seconds of render time.</p>

<p>There is an approach to this which React Suspense is using, and I wanted to try this approach for some time though - albeit as a proof-of-concept. What if we were to turn our rendering into a three-phase process? Instead of having three states (“white” for “untraversed”, “green” for “from cache” and “red” for “rendered fresh”) we would have four? The extra state (let’s call it “orange”) would mean that we could not retrieve the node from the cache, but we haven’t started rendering it either - we want to examine whether its children can be retrieved from cache first.</p>

<p>Then our traversal could work like this:</p>

<p><img src="/assets/2024-04-01-batch-caching-of-trees/wide-unfolding.png" alt="Wide Unfolding" /></p>

<p>We would go to the root node first. We would try to retrieve it from the cache, and have a miss - which would then move the node into an “unfolding” state. “Unfolding” means that we want to examine the node’s children before we try to produce a rendered result. The root node has two child nodes, which we both “unfold” as well - we thus do a second cache fetch, but we do it in a batch. These two nodes become unfolded, and they, too, are not cached. We discover that these nodes have children too, and attempt a retrieval for them. The beauty of this approach is that <strong>the number of cache fetches will be at most <em>D</em> where <em>D</em> is the depth of our render tree.</strong></p>

<p>In our “wider tree” example, we would do just 3 cache fetches - and if all nodes at a certain level of the tree get cache hits, even less:</p>

<p><img src="/assets/2024-04-01-batch-caching-of-trees/wider-unfolding.png" alt="Wider Unfolding" /></p>

<p>This allows for substantial speedups. If we go back to our previous example of <code class="language-plaintext highlighter-rouge">Site -&gt; Post -&gt; Comment</code> the number of cache roundtrips would be 3, always.</p>

<p>Moreover, we can defer the writeout of everything that has been rendered into the “pass” as well - once a level of the tree gets resolved (all nodes turn from “orange” to either “green” or “red”) that level can be written into the cache with one roundtrip as well - using <code class="language-plaintext highlighter-rouge">write_multi</code>.</p>

<p>This works using an algorithm like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">node_to_fragments</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">cache_store</span><span class="p">)</span>
  <span class="n">root_node</span> <span class="o">=</span> <span class="no">RenderNode</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>

  <span class="kp">loop</span> <span class="k">do</span> <span class="o">|</span><span class="n">n</span><span class="o">|</span>
    <span class="n">tree_of_keys</span> <span class="o">=</span> <span class="n">root_node</span><span class="p">.</span><span class="nf">collect_dependent_cache_keys</span>
    <span class="n">tree_of_values</span> <span class="o">=</span> <span class="n">hydrate</span><span class="p">(</span><span class="n">tree_of_keys</span><span class="p">,</span> <span class="n">cache_store</span><span class="p">)</span>
    <span class="n">root_node</span><span class="p">.</span><span class="nf">pushdown_values_from_cache</span><span class="p">(</span><span class="n">tree_of_values</span><span class="p">)</span>

    <span class="n">keys_to_values_for_cache</span> <span class="o">=</span> <span class="p">{}</span>
    <span class="n">root_node</span><span class="p">.</span><span class="nf">collect_rendered_caches</span><span class="p">(</span><span class="n">keys_to_values_for_cache</span><span class="p">)</span>

    <span class="n">cache_store</span><span class="p">.</span><span class="nf">write_multi</span><span class="p">(</span><span class="n">keys_to_values_for_cache</span><span class="p">)</span> <span class="k">if</span> <span class="n">keys_to_values_for_cache</span><span class="p">.</span><span class="nf">any?</span>

    <span class="k">return</span> <span class="n">root_node</span><span class="p">.</span><span class="nf">fragments</span> <span class="k">if</span> <span class="n">root_node</span><span class="p">.</span><span class="nf">done?</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>which iteratively attempts to resolve the given node until it goes from “white” to “green”, with one pass per level. This algorithm can be used, for instance, for GraphQL cache retrieval - as most objects will be cacheable by a combination of their GraphQL fields and their <code class="language-plaintext highlighter-rouge">cache_key</code> value. Same for DOM trees, same for any other tree structure with “unconditional” output.</p>

<h2 id="dealing-with-the-imperative">Dealing with the imperative</h2>

<p>Let’s loop back to the beginning. Collections with “unconditional rendering” are great and very well-suited all amazing, of course, but most Rails templates are imperative, not “containerized”. In fact, the same problem plagues the React rendering layer and Nikita recently wrote <a href="https://tonsky.me/blog/humble-vdom/">a great article</a> on a related topic, where the identity of UI components comes up as a key property for these tree-like renders.</p>

<p>But templates are not like that. They are more like that:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;h1&gt;</span>Hello, <span class="cp">&lt;%=</span> <span class="n">current_user</span><span class="p">.</span><span class="nf">display_name</span> <span class="cp">%&gt;</span><span class="nt">&lt;/h1&gt;</span>
<span class="cp">&lt;%</span> <span class="k">if</span> <span class="n">current_user</span><span class="p">.</span><span class="nf">admin?</span> <span class="cp">%&gt;</span>
  <span class="nt">&lt;button&gt;</span>Moderation tools<span class="nt">&lt;/button&gt;</span>
  <span class="nt">&lt;button&gt;</span>Scoring<span class="nt">&lt;/button&gt;</span>
<span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
</code></pre></div></div>

<p>and the like. So the template is not a “flat collection” of some kind, where the content can be recalled just by looking at the identities of the members - it is linear, executable code. Could we apply a similar optimisation to a template like this, while preserving our sanity? We could, it turns out - but that requires us to put things on their head a bit, and render asynchronously. Stay tuned for Part 2, where we are going to explore exactly that.</p>

<p>All of these concepts are applicable anywhere - I suspect they would be the most useful in the new brave world of JS frameworks, but by the same token - likely the ones that need it already are using it.</p>

<h2 id="some-code-perhaps">Some code, perhaps?</h2>

<p>Sure! You will find a proof-of-concept on Github under <a href="https://github.com/julik/unfolding">julik/unfolding</a> If you would like to explore this topic further, please do get in touch over email, or <a href="https://ruby.social/@julik">find me on Mastodon!</a></p>

<p>See you next time!</p>]]></content><author><name></name></author><summary type="html"><![CDATA[From my other posts it might seem that I am a bit of a React hater - not at all. React and related frameworks have introduced a very powerful concept into the web development field - the concept of materialised trees. In fact, we have been dealing with those in Rails for years now as well. Standard Rails rendering has been a cornerstone of dozens of applications to date, and continues to be so. But once you see “trees everywhere” it is hard not to think about optimising using trees. So let me share an idea I’ve had recently which might as well be very neat.]]></summary></entry><entry><title type="html">Testing a thousand applications with Flipper</title><link href="https://blog.julik.nl/2024/03/testing-a-thousand-applications" rel="alternate" type="text/html" title="Testing a thousand applications with Flipper" /><published>2024-03-20T00:00:00+00:00</published><updated>2024-03-20T00:00:00+00:00</updated><id>https://blog.julik.nl/2024/03/testing-a-thousand-applications</id><content type="html" xml:base="https://blog.julik.nl/2024/03/testing-a-thousand-applications"><![CDATA[<p>Feature flags are amazing. No, really, did I tell you that feature flags are amazing? They are. But <a href="/2024/03/you-might-be-running-a-few-thousand-applications">you might be running a thousand applications.</a> When this kind of complexity gets involved you might need to test combinations of feature flags, sometimes - dozens of those combinations. Exhaustive testing to the rescue!</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>As I mentioned: if you have many feature flags, sometimes your application might be dependent on the state of multiple feature flags at once. Imagine you have a feature flag called <code class="language-plaintext highlighter-rouge">deferred_checkout</code>, and another called <code class="language-plaintext highlighter-rouge">buy_one_click</code>. Since the formula for the number of possible states is <code class="language-plaintext highlighter-rouge">2 ** feature_count</code>, we know that we have a matrix of 4 possible states:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>On, On   | On, Off
Off, Off | Off, On
</code></pre></div></div>

<p>With every extra feature flag, the matrix grows with 1 row and 1 column. There is in fact a great technique for testing these types of matrices - <a href="https://www.geeksforgeeks.org/exhaustive-testing/">exhaustive testing.</a> With that technique, we can feed our software all the possible inputs, and see how it reacts.
And computers are great at enumerating large datasets. Way better than us, humans. Why not make our test suite <em>generate</em> test cases for all these combinations? When using Flipper for instance, we could then do this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">test</span> <span class="s2">"the checkout screen renders correctly"</span><span class="p">,</span> <span class="ss">feature_flags: </span><span class="p">[</span><span class="ss">:buy_one_click</span><span class="p">,</span> <span class="ss">:deferred_checkout</span><span class="p">]</span> <span class="k">do</span>
  <span class="n">get</span> <span class="s2">"/checkout"</span> <span class="c1">#...</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Thanks to the meta-programming abilities of Ruby we can put together such a helper quite easily. In your <code class="language-plaintext highlighter-rouge">test_helper.rb</code>, add the following:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">FeatureFlagCombo</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">table</span><span class="p">)</span>
    <span class="vi">@table</span> <span class="o">=</span> <span class="n">table</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">set_flags!</span>
    <span class="vi">@table</span><span class="p">.</span><span class="nf">each_pair</span> <span class="k">do</span> <span class="o">|</span><span class="n">flag</span><span class="p">,</span> <span class="n">is_enabled</span><span class="o">|</span>
      <span class="n">is_enabled</span> <span class="p">?</span> <span class="no">Flipper</span><span class="p">.</span><span class="nf">enable</span><span class="p">(</span><span class="n">flag</span><span class="p">)</span> <span class="p">:</span> <span class="no">Flipper</span><span class="p">.</span><span class="nf">disable</span><span class="p">(</span><span class="n">flag</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">to_s</span>
    <span class="vi">@table</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">flag_name</span><span class="p">,</span> <span class="n">is_enabled</span><span class="o">|</span>
      <span class="s2">"</span><span class="si">#{</span><span class="n">flag_name</span><span class="si">}</span><span class="s2">: </span><span class="si">#{</span><span class="n">is_enabled</span> <span class="p">?</span> <span class="ss">:on</span> <span class="p">:</span> <span class="ss">:off</span><span class="si">}</span><span class="s2">"</span>
    <span class="k">end</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">", "</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

</code></pre></div></div>

<p>Then we will need a method which executes a block passing it a combination of <code class="language-plaintext highlighter-rouge">on|off</code> values for every flag. This is a bit obtuse (and makes a good question
for a tech interview which you probably should not be asking): generate the entire set of possible vectors with a vector having N dimensions and values
of every dimension being restricted to a finite set.</p>

<p>The N in this case is the number of feature flags involved, and the set of possible values per
dimension is <code class="language-plaintext highlighter-rouge">[true, false]</code> - but if you ever need such a contraption for more possible values it will work just fine.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">with_every_feature_flag_combination</span><span class="p">(</span><span class="o">*</span><span class="n">feature_flags</span><span class="p">)</span>
  <span class="n">bit_values</span> <span class="o">=</span> <span class="p">[</span><span class="kp">false</span><span class="p">,</span> <span class="kp">true</span><span class="p">]</span>
  <span class="n">possible_combinations_of_enabled_and_disabled</span> <span class="o">=</span> <span class="n">bit_values</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="o">*</span><span class="p">[</span><span class="n">bit_values</span><span class="p">]</span> <span class="o">*</span> <span class="p">(</span><span class="n">feature_flags</span><span class="p">.</span><span class="nf">length</span> <span class="o">-</span> <span class="mi">1</span><span class="p">))</span>
  <span class="n">possible_combinations_of_enabled_and_disabled</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">booleans</span><span class="o">|</span>
    <span class="n">feature_combo</span> <span class="o">=</span> <span class="no">FeatureFlagCombo</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">feature_flags</span><span class="p">.</span><span class="nf">zip</span><span class="p">(</span><span class="n">booleans</span><span class="p">).</span><span class="nf">to_h</span><span class="p">)</span>
    <span class="k">yield</span><span class="p">(</span><span class="n">feature_combo</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This method will yield you a <code class="language-plaintext highlighter-rouge">FeatureFlagCombo</code> object for every such feature flag combination. If you have 2 flags - 4 <code class="language-plaintext highlighter-rouge">yield</code>s, 10 - 1024 and so forth. Then we need to extend <code class="language-plaintext highlighter-rouge">ActiveSupport::Testing::Declarative</code> to allow it to accept a keyword argument:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">test</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="ss">feature_flags: </span><span class="p">[],</span> <span class="o">&amp;</span><span class="n">block</span><span class="p">)</span>
  <span class="k">if</span> <span class="n">feature_flags</span><span class="p">.</span><span class="nf">any?</span>
    <span class="n">with_every_feature_flag_combination</span><span class="p">(</span><span class="o">*</span><span class="n">feature_flags</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">combo</span><span class="o">|</span>
      <span class="k">super</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="nb">name</span><span class="si">}</span><span class="s2"> with features </span><span class="si">#{</span><span class="n">combo</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> <span class="k">do</span>
        <span class="n">combo</span><span class="p">.</span><span class="nf">set_flags!</span>
        <span class="n">instance_exec</span><span class="p">(</span><span class="o">&amp;</span><span class="n">block</span><span class="p">)</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">else</span>
    <span class="k">super</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">block</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>and we can define our tests:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">test</span> <span class="s2">"a purchase is always refundable"</span><span class="p">,</span> <span class="ss">feature_flags: </span><span class="p">[</span><span class="ss">:discounted_purchase</span><span class="p">,</span> <span class="ss">:rapid_refund</span><span class="p">]</span> <span class="k">do</span>
  <span class="n">purchase</span> <span class="o">=</span> <span class="no">Purchase</span><span class="p">.</span><span class="nf">create!</span>
  <span class="n">assert_predicate</span> <span class="n">purchase</span><span class="p">,</span> <span class="ss">:refundable?</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This is actually where RSpec can be nicer than Minitest because of its contexts. Note that Flipper automatically installs a test helper for you, and will revert all the feature flags after every test case. <a href="https://github.com/flippercloud/flipper">Flipper is amazing.</a></p>]]></content><author><name></name></author><summary type="html"><![CDATA[Feature flags are amazing. No, really, did I tell you that feature flags are amazing? They are. But you might be running a thousand applications. When this kind of complexity gets involved you might need to test combinations of feature flags, sometimes - dozens of those combinations. Exhaustive testing to the rescue!]]></summary></entry><entry><title type="html">Tool complexity might have a cure: those pesky people who say no</title><link href="https://blog.julik.nl/2024/03/those-people-who-say-no" rel="alternate" type="text/html" title="Tool complexity might have a cure: those pesky people who say no" /><published>2024-03-19T00:00:00+00:00</published><updated>2024-03-19T00:00:00+00:00</updated><id>https://blog.julik.nl/2024/03/those-people-who-say-no</id><content type="html" xml:base="https://blog.julik.nl/2024/03/those-people-who-say-no"><![CDATA[<p>Marco Rogers started a <a href="https://ruby.social/@polotek@social.polotek.net/112107249155406279">remarkable thread</a> on Mastodon, which absolutely struck it home for me. Teams absolutely <em>do</em> get mired up in complex tooling. They absolutely <em>can</em> be unprepared, and there absolutely <em>is</em> a skew between the newly-minted “frontend” and “backend” ecosystems. I might have a few things to say about this.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>There is an important point that <a href="https://macwright.com/2023/12/31/luxury-of-simplicity">simplicity is luxury</a> and I am not disputing it. What I am disputing, however, is the notion that complexity cannot be contained. That complexity - and tool complexity specifically - is something we should just “get on with”.</p>

<p>And complexity has end-user impact too. Just a few articles to whet your appetite:</p>

<ul>
  <li><a href="https://infrequently.org/2023/02/the-market-for-lemons/">The Market for Lemons</a> by Alex Russell</li>
  <li><a href="https://tonsky.me/blog/js-bloat/">JavaScript Bloat in 2024</a> by Nikita</li>
  <li><a href="https://www.mayank.co/blog/react-server-components/">React Server Components: the Good, the Bad, and the Ugly</a> by Mayank</li>
  <li>and many, many more.</li>
</ul>

<p>I don’t have exhaustive solutions or exact causation. But I do have a hypothesis of what might be one of the root causes of this complexity - it is that <a href="https://blog.julik.nl/2021/10/why-we-cant-have-proper-mentorship">hands-off engineering management is a problem.</a> Yes, caring about people, doing 1:1s, talking to stakeholders, determining career ladders and writing quarterly budgets and roadmaps is very important. But let’s not forget: software gets <em>created.</em> To create software, we often bump into choosing prior art - either to use, or to copy, or to study. There is a very strong undercurrent in the “modern scale-up space”, which goes roughly like this:</p>

<ul>
  <li>People and teams need autonomy, so let them use what they pick</li>
  <li>Individuals within teams need to be empowered, so let them use what they pick</li>
  <li>We care about engineers being employable if they leave. We can’t make them code in COBOL forever just because if it happens to be our “golden path” at the company.</li>
  <li>We have time - and budget - to experiment with all of that. <a href="https://gigamonkeys.com/flowers/">Let a 1000 flowers bloom</a> and then we just trample over what remains and form an architecture guild to determine what “the golden path” should be</li>
  <li>Fresh hires will bring expertise in their favourite tools. We will disenfranchise them if we forbid these “brought along tools” from getting used.</li>
  <li>We won’t be able to hire and retain talent if we force them to use older, less-fancy tools.</li>
</ul>

<p>These are workable propositions. And they are humanistic in the end - individuals on the team - and teams - can feel empowered to pick their own tooling. You don’t need to get a stamp of approval to try that new database. No directorial mandate to install that new linter. No guild confirmation to try rewriting a few modules in TypeScript. And it <em>will</em> make you more employable if you learn and deploy the latest alpha version of next.js instead of that antiquated pile of goop in Laravel or Rails that actually happens to earn your current company money.</p>

<p>But there are assumptions embedded in those propositions which are rapidly getting less and less certain.</p>

<ul>
  <li>We have enough people to have a “frontend guild” and a “backend guild” and an “every-little-other-thing guild” to deliberate over those decisions</li>
  <li>We have people who will retool away from “the fancy new little thing” if it turns out to be sour grapes</li>
  <li>We keep teams stable and do not reorg frequently, there are no layoffs and little churn. A person doing a retool can <em>focus</em> not only on the retooling itself, but also on the fixing of the myriad of edge cases that emerge, on building up expertise, on documentation - and ultimately on sunsetting that tool if it turns out to be the wrong choice. They will not move on to their next backlog grooming sesh at the scrum factory in 2 weeks.</li>
  <li>We have enough time to help the person who misjudged an implementation or a tool choice revert. We don’t need to dump accountability on the person themselves - they have so much to do, also with all this feature dev going on!</li>
</ul>

<p>And this all is fine-and-well as long as these assumptions hold. But the money is running out. Investors are getting impatient. The feature factory wheels have to turn faster, reorgs have to speed up - do we really <em>need</em> to do that nuxt-to-next transition, or move from REST to GraphQL, or do a monorepo?</p>

<p>All the while, there are folks in your org who knew that it would come to this. That guy who wrote the first version of the product in bare PHP, who was seen lamenting that “why is this team trying create-react-app instead of building proper browsing history support”. The lady who was asking “do we really need to use Kubernetes if we ever run just 4 servers?” The two fellows who were saying “probably Redux is going to go out of fashion soon, maybe it is not such a good idea to go all-in on it”. They were all so annoying, remember? They were no managers - and if they were, their job would have been to talk to stakeholders and do 1:1s. Them telling other engineers “not to use that thing, because it is probably irresponsible” would be “micromanaging” - and they would be entering the role of <a href="https://lethain.com/tech-lead-managers/">a tech lead</a> role which is a trap.</p>

<p>We have effectively divorced experience from authority. And what happens when you divorce experience and authority in a production environment is that people will make worse choices. Not because they are bad at their job - not at all. But because they haven’t seen <em>how bad</em> tech debt can get given a long-enough streak of mistaken technology choices and experiments, and because they do not necessarily have the autonomy (and the knowledge) to work themselves out of a corner if they paint themselves into one. Seeing that corner can be difficult. Investment into an approach can be high. And it can be done for all the different reasons:</p>

<ul>
  <li>“I want to build X with my favourite tech, because I know it inside-out” - but then you get moved to a different team, and yet another freshly formed team gets formed to support X. They carry your can.</li>
  <li>“Learning this tech and building X in it will make me more employable” - sure, and we know that “working at the same place forever” is a losing economic strategy. But once you do build up that experience and do leave, the people supporting X will be stuck with your choices</li>
  <li>“Know so-and-so are touting that this tech is Just The Best Thing Ever™®©, period. It is an industry standard!” - well, how about those people being employed <a href="https://vercel.com">at places where there are incentives to sell you this tech and to create moat?</a> What if what you are getting sold services instead of “the right tool for the job”? Everyone can be gullible given enough persuasion.</li>
  <li>“This Y tech we use here is so old, barely anyone wants to touch it” - Tech being “old” does not mean anything on its own. Software <em>rots</em> - that is important. Are you dealing with rotting software (viscosity, rigidity, brittleness) or truly with an unsupported and unusable tech?</li>
</ul>

<p>The issue is that we have removed people saying those things from the decision loop. If the pace is fast enough, if the pressure is on - and the VC money amply available - on a freshly formed team the likely outcome will be “as long as X gets built by end of Q3” everyone gets what they want. Stakeholders get their feature. Some folks on the team earn promotions. Some get to try the tech they aspired to try. But this assumes that the software does not get touched later, does not get expanded, does not get worked on - it is a very wasteful approach to tools and to the product. But the guy who would tell you is gone already - remember how he got dragged to HR for saying “we should not write any Cucumber here because it is nonsense!” He got the message, and after a brief PIP has left for greener pastures.</p>

<p>The folks on the team might simply <em>not be prepared</em> to revert out of mistaken tech choices. They do not have the authority, the experience and the time to be truly accountable for these choices. “Being accountable” includes “this was a terrible idea, let’s rip this out” too. “Being accountable” includes “this tech migration is a dead end and is costing us too much, let’s abort and backtrack”. “Being accountable” includes “I like that other tech better, but this tech is already in use and pays our salaries”. And yes - “Being accountable” includes saying “this was <em>my</em> decision, I take full responsibility for it on a personal level and I regret it, how do we revert?”, a.k.a. having the gusto to say <em>I fucked up.</em></p>

<p>This, right here:</p>

<iframe src="https://social.polotek.net/@polotek/112106979666938989/embed" width="400" allowfullscreen="allowfullscreen" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms"></iframe>

<p>I am not trying to convince you that you must have a grumpy senior engineer on your team who is going to forbid the youngsters to play with new toys. I am not advocating for keeping toxic people around. What I <em>am</em> advocating, however, is that we abandon this notion that decisions about picking tech on teams are done either by-commitee or by separate team members for their personal enjoyment. We need folks respected enough (and experienced enough) that their “No” would not be a gatekeeper, “old man yells at cloud” type of “no”. Not a cry for “how everything used to be better”. We need their “No” to be the “there be dragons” kind. The “we will be starting on this and busy with it for 2 years and not be any better off for it” kind.</p>

<p>There was a great point in the discussion: whether backend folks escaped the “tooling trap” and the “retooling trap”. They did not. But the Cambrian explosion of tooling in backend went about around the early 2000s - when everyone was trying to sell you SOAP, and Middleware™, and J2EE, and many other amazing things. Back then quite some people started working on small teams where using all of that was simply <em>too much</em> - and they were solely responsible for delivery. Being personally accountable and responsible does get you places. If there is no “frontend guild” to pull you out of a trough where you got yourself by trying out React server components, you will try them (or similar tech) a couple of times, and learn both what the strengths and weaknesses are, and <em>how to revert out of it.</em> That <em>reverting</em> requires both experience and autonomy. You can give people space to acquire that experience on your team, but it takes time - and budget. And it takes actual, applied accountability - a.k.a. “you broke it, you fix it”. I, personally, <em>never</em> use fancy cloud databases <em>exactly</em> because I am accountable for them not breaking on us. I am accountable for <em>reverting</em> out of them if they don’t work, or cost too much. I am <em>accountable</em> for every member of my team spending extra 10 seconds on every Git commit because suddenly your entire codebase runs Prettier through a commit hook (I would not allow such a thing, and - yes - this is a real story). And I could never give my team a guarantee of that support if I were only allowed to discuss “how do you see yourself at this job in 5 years”. We don’t have 5 years. We have a week.</p>

<p>There is some more color to add to the question of <a href="https://mastodon.social/@floby/112107065645789780">Web/UI engineers being often regarded less than other kinds of engineers</a> though. I have never <em>seen</em> this being the case (lucky, I know). What I did see is that we have devalued full-stack web engineers, because “they would be bad at both”, because “modern frontend is too complex”. Where that has led is that the (not-at-all-)mythical full stack engineers are <em>exactly</em> the people who would YAGNI at you for using all this fancy stuff you so ardently advocate for. Because from a full-stack perspective a lot of problems in frontend are not that hard - if you approach them holistically. No, we are not going to use a programming language’s <code class="language-plaintext highlighter-rouge">import</code> keyword to bundle SVG animations into our source code. No, we are not going to be rendering JSON fragments when the user already gets complete HTML. No, we are not going to create a whole new API just to load one flat gallery of photos - a pre-rendered <code class="language-plaintext highlighter-rouge">div</code> will do just fine! Once those people get reprimanded often enough - they will stop speaking.</p>

<p>Another tricky part with bad FE decisions being hard to revert from is that <em>there is just way more of frontend.</em> React specifically encourages you to make smaller and smaller components, touting composition as the benefit you will get from that. And you will, but you will find yourself with a codebase which has thousands of components. Good luck codemodding/changing them all in a safe, compatible way - and if you used something like Redux it is likely that a few hundreds of those components will depend on Redux, in an intricate way too. With backend work, you can sometimes rip out an entire service and replace it with another (if you haven’t allowed very complex interfaces into it) - not so with a thousand components, which also need to be updated incrementally.</p>

<p>Note that I am not saying that “all this new fancy FE stuff is just too fancy” - not at all. The React crowd have done some exceptional work with tackling some very real complexity <em>for specific types of software.</em> A lot of what they learned applies elsewhere, and can - and should - be amalgamated into other technology we use. I am not saying that “we should use less fancy tech” just because <a href="https://dev.to/ichater/david-heinemeier-hansson-removes-typescript-from-turbo-8-an-unbiased-take-2c0d">I would want to remove tech introduced by people who disparage me and quit my company.</a> I am talking about the fact that you need people on your team (or teams) who <em>both</em> do hands-on work - and do it well, <em>and</em> are empowered to make decisions, with empathy.</p>

<p>And yes, sometimes they will say “no” to that new fancy linter/JS router/Rust WASM module/Neo4J. They likely mean it well. The <a href="https://medium.com/technology-hits/transition-in-tech-from-luxury-perks-to-sweeping-layoffs-the-zirp-era-ends-1f8ecabfc131">free lunch is just about over</a> and their insights may prove very valuable.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Marco Rogers started a remarkable thread on Mastodon, which absolutely struck it home for me. Teams absolutely do get mired up in complex tooling. They absolutely can be unprepared, and there absolutely is a skew between the newly-minted “frontend” and “backend” ecosystems. I might have a few things to say about this.]]></summary></entry><entry><title type="html">Your might be running a thousand applications</title><link href="https://blog.julik.nl/2024/03/you-might-be-running-a-few-thousand-applications" rel="alternate" type="text/html" title="Your might be running a thousand applications" /><published>2024-03-02T00:00:00+00:00</published><updated>2024-03-02T00:00:00+00:00</updated><id>https://blog.julik.nl/2024/03/you-might-be-running-a-few-thousand-applications</id><content type="html" xml:base="https://blog.julik.nl/2024/03/you-might-be-running-a-few-thousand-applications"><![CDATA[<p>Feature flags are awesome. But just like user preferences or settings they have the tendency of turning your application into multiple applications, all embedded in one.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>A feature flag is a <strong>bit</strong> (an on/off switch), and for any given user, it can be either on or off. But it helps to be mindful of the following: the number of possible states of any given user is actually a product of all your feature flags. There is a handy formula for calculating the total number of possible states your application may be for any given user depending on the feature flags being on or off, and it goes like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2 ** num_feature_flags
</code></pre></div></div>

<p>(<code class="language-plaintext highlighter-rouge">**</code> is “raised to the power of” in Ruby-speak). So, if your application has 10 feature flags: congratulations, any user might be running nog the application you are building and testing, but one of its 1024 different variations. This leads to a combinatorial explosion of things you need to test, and worse even - to <a href="https://www.hillelwayne.com/post/feature-interaction/">feature interaction bugs.</a> If your development process is very rapid, it will not take long before everyone just gives up testing <em>everything</em> in <em>every</em> combination of the feature flags, and some bugs will invariably seep through.</p>

<p>It is not all doom and gloom, however. Here are the rules that I follow to reduce the potential blast radius of those things:</p>

<ul>
  <li>Use feature flags either “all the way left” (in some high-level module at the start of any given user flow) or “all the way right” (at the deepest call site). Definitely not both and not “in the middle”</li>
  <li>Try to localize feature flags to one domain of the application. If it has to do with checkouts - do not use the feature flag anywhere except within checkouts</li>
  <li>Delete feature flags at the earliest opportunity</li>
</ul>

<p>I don’t subscribe to the notion <a href="https://zaidesanton.substack.com/p/feature-flags-are-ruining-your-codebase">that feature flags bring ruin</a> - they are an excellent tool. Just be mindful of the power factor.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Feature flags are awesome. But just like user preferences or settings they have the tendency of turning your application into multiple applications, all embedded in one.]]></summary></entry><entry><title type="html">Changing your mind is not my job</title><link href="https://blog.julik.nl/2024/02/change-my-mind" rel="alternate" type="text/html" title="Changing your mind is not my job" /><published>2024-02-29T00:00:00+00:00</published><updated>2024-02-29T00:00:00+00:00</updated><id>https://blog.julik.nl/2024/02/change-my-mind</id><content type="html" xml:base="https://blog.julik.nl/2024/02/change-my-mind"><![CDATA[<p>There is a <a href="https://www.thelist.com/593835/the-origins-of-the-change-my-mind-meme/">popular meme that has been going around for years now,</a> This is in fact close to heart for every passionate technologist. Most of use have either been the guy at the desk, or an innocent passer-by willing to enter the conversation. With fairly expected results.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>It was sometimes around 2004 or 2005 that I had the privilege of attending my first Ruby meetup, where I finally met the <a href="https://fngtps.com/">Fingertips</a> fellows. Before the meetup I have posted an announcement on a forum I was frequenting at the time.</p>

<p>Not a half an hour later I have received an email with roughly the following content:</p>

<blockquote>
  <p>✉️<br />
Hi! 
I see that you would like to do a presentation about Rails and Ruby. 
I am currently using PHP. Please convince me that I need to switch - 
because it isn’t any better than PHP, right? I mean, why would I?<br />
Cheers,<br />
so and so</p>
</blockquote>

<p>Now, this was in Russian so the exact wording was a bit different. And Eastern-European people (myself included) often fall into the trap of “proving someone wrong on the Internet”, and into the trap of “teaching others how they are misguided”, and other traps we won’t get to right now. But still: this struck a nerve.</p>

<p>There are ways to ask questions. A multitude, in fact. And other people’s opinions are genuinely interesting and can help us uncover blind spots in our own thinking. Some detail we have overlooked. A different angle from a divergent societal group. An unintended consequence. A piece of context we could have just failed to register. The issue then lies with the <em>intent</em> of asking a question.</p>

<p>You can ask a question and be ready to listen to the response - solicit opinions, if you will. The most important thing, in that setting, is to <em>listen to the opinion.</em> It is not the matter of them convincing you of something, because it would not be an exchange of opinions - it would be debate. A debate can get real adversarial, real quick.</p>

<p>Therefore, it is very, very important not to jump to refutations of the opinions you disagree with. For disagreeing, there is time and place. If you want to “hash out” a problem - sure, have at it. Get into a conversation with a clear, demonstrated intent of “hashing this thing out”. Then both sides bring their points, there is a deadline to end the conversation (there is? right?), and there is an expectation of a friendly, but firm argument. When you just ask for opinions, or are genuinely curious about the other person’s point of view, you are not obligated to either affirm or refute their opinion. Just listen, make mental notes, and see whether they are saying something that resonates.</p>

<p>Being a good listener is crucial in those situations. Especially if the other person seems to be woefully wrong, uninformed and non-empathetical. Exactly those are moments when listening matters. The “but… but!…” can be saved for another time. Remember that the other person is likely spending time on providing their opinion too. They might not have time, nor the energy, to enter a fully fledged debate. They may hold conflicting views. If you are going into it for the <em>lulz</em> - sure, go ahead, but don’t feel bad if they refuse to participate.</p>

<hr />

<p>And so, on that evening in the early-aughts, I sent a reply:</p>

<blockquote>
  <p>✉️<br />
Dear so and so,
Thank you for your interest. Unfortunately I do not have the time, nor 
the inclination, to convince anyone of anything at the moment. I have no 
obligation to prove anything to you, and I wish you all the success 
with the tools you are already happily using.<br />
Have a great day.<br />
Julik.</p>
</blockquote>

<p>And that was that.</p>

<p><a href="https://twitter.com/julikt/status/1763163642915074167">Inspired by.</a></p>]]></content><author><name></name></author><summary type="html"><![CDATA[There is a popular meme that has been going around for years now, This is in fact close to heart for every passionate technologist. Most of use have either been the guy at the desk, or an innocent passer-by willing to enter the conversation. With fairly expected results.]]></summary></entry><entry><title type="html">Versioned business logic with ActiveRecord</title><link href="https://blog.julik.nl/2024/02/versioned-business-logic" rel="alternate" type="text/html" title="Versioned business logic with ActiveRecord" /><published>2024-02-27T00:00:00+00:00</published><updated>2024-02-27T00:00:00+00:00</updated><id>https://blog.julik.nl/2024/02/versioned-business-logic</id><content type="html" xml:base="https://blog.julik.nl/2024/02/versioned-business-logic"><![CDATA[<p>Every succesful application evolves. Business logic is often one of the things that evolves the most, but it is customary to have data which changes over time. Sometimes - over months or years. A lot of spots have logic related to “data over time”. For example: you collect payments from users, but some users were not getting charged VAT. Your new users need to get charged VAT, but they will also pay more, but you want to “grandfather” your existing users into a pricing plan where VAT is included in their pricing, so that the amount they pay does not change.</p>

<p>ActiveRecord, by default, is not very conductive to such changes, but I have recently discovered a very nice pattern for adding versioned logic to models.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>Let’s go at the VAT example. Our previous business logic might have looked like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Customer</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="k">def</span> <span class="nf">charge!</span>
    <span class="n">monthly_charge</span> <span class="o">=</span> <span class="n">pricing_plan</span><span class="p">.</span><span class="nf">monthly_price</span>
    <span class="n">ledger_account</span><span class="p">.</span><span class="nf">debit!</span><span class="p">(</span><span class="n">monthly_charge</span><span class="p">)</span>
    <span class="no">OurCompany</span><span class="p">.</span><span class="nf">ledger_account</span><span class="p">.</span><span class="nf">credit!</span><span class="p">(</span><span class="n">monthly_charge</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now we are introducing VAT per locality, and we want our pricing to be “net” pricing. On top of that, we want to put VAT that we charge users into a special ledger account, so that VAT returns can be done in an easier way:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Customer</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="k">def</span> <span class="nf">charge!</span>
    <span class="n">monthly_charge</span> <span class="o">=</span> <span class="n">pricing_plan</span><span class="p">.</span><span class="nf">monthly_price</span>
    <span class="n">vat</span> <span class="o">=</span> <span class="n">monthly_charge</span> <span class="o">*</span> <span class="n">pricing_plan</span><span class="p">.</span><span class="nf">vat_ratio_for</span><span class="p">(</span><span class="nb">self</span><span class="p">)</span>

    <span class="n">ledger_account</span><span class="p">.</span><span class="nf">debit!</span><span class="p">(</span><span class="n">monthly_charge</span><span class="p">)</span>
    <span class="n">ledger_account</span><span class="p">.</span><span class="nf">debit!</span><span class="p">(</span><span class="n">vat</span><span class="p">)</span>

    <span class="no">OurCompany</span><span class="p">.</span><span class="nf">ledger_account</span><span class="p">.</span><span class="nf">credit!</span><span class="p">(</span><span class="n">monthly_charge</span><span class="p">)</span>
    <span class="no">OurCompany</span><span class="p">.</span><span class="nf">vat_returns_account</span><span class="p">.</span><span class="nf">credit!</span><span class="p">(</span><span class="n">vat</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>That is a <code class="language-plaintext highlighter-rouge">Customer</code> of a different <em>version</em> - it is not the pricing plan that changes, but the business logic (the way) of how we <code class="language-plaintext highlighter-rouge">charge!</code> the customer. There is, in fact, a pattern which is perfect for this sort of thing - the Strategy pattern. Let’s make the <code class="language-plaintext highlighter-rouge">charge!</code> logic versionable:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">change</span>
  <span class="n">add_column</span> <span class="ss">:customers</span><span class="p">,</span> <span class="ss">:billing_strategy_module_name</span><span class="p">,</span> <span class="ss">:string</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">"BillingStrategyV1"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>and extract our first strategy (for old customers) into this billing strategy:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">BillingStrategyV1</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">charge!</span><span class="p">(</span><span class="n">customer</span><span class="p">)</span>
    <span class="n">monthly_charge</span> <span class="o">=</span> <span class="n">customer</span><span class="p">.</span><span class="nf">pricing_plan</span><span class="p">.</span><span class="nf">monthly_price</span>
    <span class="n">customer</span><span class="p">.</span><span class="nf">ledger_account</span><span class="p">.</span><span class="nf">debit!</span><span class="p">(</span><span class="n">monthly_charge</span><span class="p">)</span>
    <span class="no">OurCompany</span><span class="p">.</span><span class="nf">ledger_account</span><span class="p">.</span><span class="nf">credit!</span><span class="p">(</span><span class="n">monthly_charge</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">module</span> <span class="nn">BillingStrategyV2</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">charge!</span><span class="p">(</span><span class="n">customer</span><span class="p">)</span>
    <span class="n">monthly_charge</span> <span class="o">=</span> <span class="n">customer</span><span class="p">.</span><span class="nf">pricing_plan</span><span class="p">.</span><span class="nf">monthly_price</span>
    <span class="n">vat</span> <span class="o">=</span> <span class="n">monthly_charge</span> <span class="o">*</span> <span class="n">customer</span><span class="p">.</span><span class="nf">pricing_plan</span><span class="p">.</span><span class="nf">vat_ratio_for</span><span class="p">(</span><span class="n">customer</span><span class="p">)</span>

    <span class="n">customer</span><span class="p">.</span><span class="nf">ledger_account</span><span class="p">.</span><span class="nf">debit!</span><span class="p">(</span><span class="n">monthly_charge</span><span class="p">)</span>
    <span class="n">customer</span><span class="p">.</span><span class="nf">ledger_account</span><span class="p">.</span><span class="nf">debit!</span><span class="p">(</span><span class="n">vat</span><span class="p">)</span>

    <span class="no">OurCompany</span><span class="p">.</span><span class="nf">ledger_account</span><span class="p">.</span><span class="nf">credit!</span><span class="p">(</span><span class="n">monthly_charge</span><span class="p">)</span>
    <span class="no">OurCompany</span><span class="p">.</span><span class="nf">vat_returns_account</span><span class="p">.</span><span class="nf">credit!</span><span class="p">(</span><span class="n">vat</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">Customer</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="k">def</span> <span class="nf">charge!</span>
    <span class="n">billing_strategy_module_name</span><span class="p">.</span><span class="nf">constantize</span><span class="p">.</span><span class="nf">charge!</span><span class="p">(</span><span class="nb">self</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now we can “move” our customers from one version of the business logic to another:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">customer</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">billing_strategy_module_name: </span><span class="s2">"BillingStrategyV2"</span><span class="p">)</span>
</code></pre></div></div>

<p>Implementing “grandfathering” in this setup becomes trivial:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">BillingStrategyV1B</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">charge!</span><span class="p">(</span><span class="n">customer</span><span class="p">)</span>
    <span class="n">monthly_charge</span> <span class="o">=</span> <span class="n">customer</span><span class="p">.</span><span class="nf">pricing_plan</span><span class="p">.</span><span class="nf">monthly_price</span>
    <span class="n">vat_ratio</span> <span class="o">=</span> <span class="n">monthly_charge</span> <span class="o">*</span> <span class="n">customer</span><span class="p">.</span><span class="nf">pricing_plan</span><span class="p">.</span><span class="nf">vat_ratio_for</span><span class="p">(</span><span class="n">customer</span><span class="p">)</span>
    <span class="n">net_charge</span> <span class="o">=</span> <span class="n">monthly_charge</span> <span class="o">/</span> <span class="p">(</span><span class="mf">1.0</span> <span class="o">+</span> <span class="n">vat_ratio</span><span class="p">)</span>
    <span class="n">vat</span> <span class="o">=</span> <span class="n">net_charge</span> <span class="o">*</span> <span class="n">vat_ratio</span>

    <span class="n">customer</span><span class="p">.</span><span class="nf">ledger_account</span><span class="p">.</span><span class="nf">debit!</span><span class="p">(</span><span class="n">net_charge</span><span class="p">)</span>
    <span class="n">customer</span><span class="p">.</span><span class="nf">ledger_account</span><span class="p">.</span><span class="nf">debit!</span><span class="p">(</span><span class="n">vat</span><span class="p">)</span>

    <span class="no">OurCompany</span><span class="p">.</span><span class="nf">ledger_account</span><span class="p">.</span><span class="nf">credit!</span><span class="p">(</span><span class="n">net_charge</span><span class="p">)</span>
    <span class="no">OurCompany</span><span class="p">.</span><span class="nf">vat_returns_account</span><span class="p">.</span><span class="nf">credit!</span><span class="p">(</span><span class="n">vat</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>If the methods you used to call inside <code class="language-plaintext highlighter-rouge">charge!</code> are public - congratulations, you are done!</p>

<h3 id="dealing-with-private-methods">Dealing with private methods</h3>

<p>An obvious issue here is that this “evolving” business logic often does not start versioned. And the method we want to version might as well be using ActiveRecord internals, for a good reason too. For example, imagine our first version looks like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Customer</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">after</span> <span class="ss">:update</span><span class="p">,</span> <span class="ss">:update_portfolio_value</span>

  <span class="k">def</span> <span class="nf">update_portfolio_value</span>
    <span class="n">total_value_was</span><span class="p">,</span> <span class="n">total_value_has_become</span> <span class="o">=</span> <span class="n">attribute_was</span><span class="p">(</span><span class="ss">:total_value</span><span class="p">),</span> <span class="n">total_value</span>
    <span class="n">delta</span> <span class="o">=</span> <span class="n">total_value_has_become</span> <span class="o">-</span> <span class="n">total_value_was</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">portfolio_value</span> <span class="o">+=</span> <span class="n">delta</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>and our second version (for customers in draconian stock option buyout regimes in the EU) must become:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Customer</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">after</span> <span class="ss">:update</span><span class="p">,</span> <span class="ss">:update_portfolio_value</span>

  <span class="k">def</span> <span class="nf">update_portfolio_value</span>
    <span class="n">total_value_was</span><span class="p">,</span> <span class="n">total_value_has_become</span> <span class="o">=</span> <span class="n">attribute_was</span><span class="p">(</span><span class="ss">:total_value</span><span class="p">),</span> <span class="n">total_value</span>
    <span class="n">delta</span> <span class="o">=</span> <span class="n">total_value_has_become</span> <span class="o">-</span> <span class="n">total_value_was</span>
    <span class="n">tax</span> <span class="o">=</span> <span class="n">calculate_tax_over_potential_earnings</span><span class="p">(</span><span class="n">delta</span><span class="p">)</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">portfolio_value</span> <span class="o">+=</span> <span class="n">delta</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">potential_tax_due</span> <span class="o">+=</span> <span class="n">tax</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>We could use our approach with the <code class="language-plaintext highlighter-rouge">Struct</code> here, were it not so that we are using <code class="language-plaintext highlighter-rouge">attribute_was</code>. <code class="language-plaintext highlighter-rouge">attribute_was</code> is a private ActiveRecord method - you can use it from the “inside” of the model, but a <code class="language-plaintext highlighter-rouge">Struct</code> - even one defined inside the <code class="language-plaintext highlighter-rouge">Customer</code> module namespace - won’t be permitted to call it. I won’t discuss the benefits/downsides of module privates and will refer you <a href="https://steve-yegge.blogspot.com/2010/07/wikileaks-to-leak-5000-open-source-java.html">to my favorite article on the subject</a> instead. But how do we implement it?</p>

<p>We know that we want an object “somewhere” which responds to <code class="language-plaintext highlighter-rouge">attribute_was</code> and forwards it to the <code class="language-plaintext highlighter-rouge">Customer</code> model. Ideally, we would do this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">PortfolioStrategyV1</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">update_portfolio_value</span><span class="p">(</span><span class="n">customer</span><span class="p">)</span>
    <span class="n">total_value_was</span><span class="p">,</span> <span class="n">total_value_has_become</span> <span class="o">=</span> <span class="n">some_shim_for_private_methods</span><span class="p">(</span><span class="n">customer</span><span class="p">).</span><span class="nf">attribute_was</span><span class="p">(</span><span class="ss">:total_value</span><span class="p">),</span> <span class="n">customer</span><span class="p">.</span><span class="nf">total_value</span>
    <span class="n">delta</span> <span class="o">=</span> <span class="n">total_value_has_become</span> <span class="o">-</span> <span class="n">total_value_was</span>
    <span class="n">customer</span><span class="p">.</span><span class="nf">portfolio_value</span> <span class="o">+=</span> <span class="n">delta</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">module</span> <span class="nn">PortfolioStrategyV2</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">update_portfolio_value</span><span class="p">(</span><span class="n">customer</span><span class="p">)</span>
    <span class="n">total_value_was</span><span class="p">,</span> <span class="n">total_value_has_become</span> <span class="o">=</span> <span class="n">some_shim_for_private_methods</span><span class="p">(</span><span class="n">customer</span><span class="p">).</span><span class="nf">attribute_was</span><span class="p">(</span><span class="ss">:total_value</span><span class="p">),</span> <span class="n">customer</span><span class="p">.</span><span class="nf">total_value</span>
    <span class="n">delta</span> <span class="o">=</span> <span class="n">total_value_has_become</span> <span class="o">-</span> <span class="n">total_value_was</span>
    <span class="n">tax</span> <span class="o">=</span> <span class="n">customer</span><span class="p">.</span><span class="nf">calculate_tax_over_potential_earnings</span><span class="p">(</span><span class="n">delta</span><span class="p">)</span>
    <span class="n">customer</span><span class="p">.</span><span class="nf">portfolio_value</span> <span class="o">+=</span> <span class="n">delta</span>
    <span class="n">customer</span><span class="p">.</span><span class="nf">potential_tax_due</span> <span class="o">+=</span> <span class="n">tax</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">Customer</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">after</span> <span class="ss">:update</span><span class="p">,</span> <span class="ss">:update_portfolio_value</span>

  <span class="k">def</span> <span class="nf">update_portfolio_value</span>
    <span class="n">portfolio_value_strategy</span><span class="p">.</span><span class="nf">constantize</span><span class="p">.</span><span class="nf">update_portfolio_value</span><span class="p">(</span><span class="nb">self</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Note that in addition to introducing our shim, we would need to replace all instances of <code class="language-plaintext highlighter-rouge">self</code> with <code class="language-plaintext highlighter-rouge">customer</code> and we are losing “implicit self”, which is a substantial ergonomic advantage. This means that when you extract the <code class="language-plaintext highlighter-rouge">update_portfolio_value</code> method you won’t be able to just copy and paste it into your strategy, but you will need to do replacements. And that shim… could we do without?</p>

<h3 id="delegate-delegate-delegate">Delegate, delegate, delegate</h3>

<p>Ruby has a neat module for this in its standard library - <code class="language-plaintext highlighter-rouge">SimpleDelegator</code>. The delegator wraps an object, and forwards calls it does not override to that object - which allows for very good composition.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">PortfolioStrategyV1</span> <span class="o">&lt;</span> <span class="no">SimpleDelegator</span>
  <span class="k">def</span> <span class="nf">update_portfolio_value</span>
    <span class="n">total_value_was</span><span class="p">,</span> <span class="n">total_value_has_become</span> <span class="o">=</span> <span class="n">some_shim_for_private_methods</span><span class="p">(</span><span class="n">__getobj__</span><span class="p">).</span><span class="nf">attribute_was</span><span class="p">(</span><span class="ss">:total_value</span><span class="p">),</span> <span class="n">customer</span><span class="p">.</span><span class="nf">total_value</span>
    <span class="n">delta</span> <span class="o">=</span> <span class="n">total_value_has_become</span> <span class="o">-</span> <span class="n">total_value_was</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">portfolio_value</span> <span class="o">+=</span> <span class="n">delta</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">PortfolioStrategyV2</span> <span class="o">&lt;</span> <span class="no">SimpleDelegator</span>
  <span class="k">def</span> <span class="nf">update_portfolio_value</span>
    <span class="n">total_value_was</span><span class="p">,</span> <span class="n">total_value_has_become</span> <span class="o">=</span> <span class="n">some_shim_for_private_methods</span><span class="p">(</span><span class="n">__getobj__</span><span class="p">).</span><span class="nf">attribute_was</span><span class="p">(</span><span class="ss">:total_value</span><span class="p">),</span> <span class="n">total_value</span>
    <span class="n">delta</span> <span class="o">=</span> <span class="n">total_value_has_become</span> <span class="o">-</span> <span class="n">total_value_was</span>
    <span class="n">tax</span> <span class="o">=</span> <span class="n">calculate_tax_over_potential_earnings</span><span class="p">(</span><span class="n">delta</span><span class="p">)</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">portfolio_value</span> <span class="o">+=</span> <span class="n">delta</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">potential_tax_due</span> <span class="o">+=</span> <span class="n">tax</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>To change our code in to use delegation, we will need to revize our method like so:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Customer</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">after</span> <span class="ss">:update</span><span class="p">,</span> <span class="ss">:update_portfolio_value</span>

  <span class="k">def</span> <span class="nf">update_portfolio_value</span>
    <span class="n">portfolio_value_strategy</span><span class="p">.</span><span class="nf">constantize</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="nb">self</span><span class="p">).</span><span class="nf">update_portfolio_value</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>But there is a catch. We are still having to deal with the <code class="language-plaintext highlighter-rouge">some_shim_for_private_methods(__getobj__)</code>, which means that once the logic becomes versioned - we will need to edit the method when placing it into a strategy. This is actually a spot where we can use some Ruby magic to allow us access to the private methods inside of our delegator. <strong>Become ungovernable</strong> and all - but seriously, this is useful. Let’s create an <code class="language-plaintext highlighter-rouge">AccursedDelegator</code> (props to <a href="https://wjwh.eu">Wander</a> for the name):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">AccursedDelegator</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">model</span><span class="p">)</span>
    <span class="vi">@model</span> <span class="o">=</span> <span class="n">model</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">respond_to_missing?</span><span class="p">(</span><span class="n">method_name</span><span class="p">,</span> <span class="n">including_private_methods</span><span class="p">)</span>
    <span class="c1"># Below comes the only meaningful difference with SimpleDelegator</span>
    <span class="c1"># - we allow the `including_private_methods` variable to get passed</span>
    <span class="c1"># through to the delegated object</span>
    <span class="vi">@model</span><span class="p">.</span><span class="nf">respond_to?</span><span class="p">(</span><span class="n">method_name</span><span class="p">,</span> <span class="n">including_private_methods</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">method_missing</span><span class="p">(</span><span class="n">method_name</span><span class="p">,</span> <span class="o">...</span><span class="p">)</span>
    <span class="vi">@model</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span><span class="n">method_name</span><span class="p">,</span> <span class="o">...</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Once we have this, our strategies can drop all the indirections and operate “as if” they existed on the <code class="language-plaintext highlighter-rouge">Customer</code> model itself:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">PortfolioStrategyV1</span> <span class="o">&lt;</span> <span class="no">AccursedDelegator</span>
  <span class="k">def</span> <span class="nf">update_portfolio_value</span>
    <span class="n">total_value_was</span><span class="p">,</span> <span class="n">total_value_has_become</span> <span class="o">=</span> <span class="n">attribute_was</span><span class="p">(</span><span class="ss">:total_value</span><span class="p">),</span> <span class="n">total_value</span>
    <span class="n">delta</span> <span class="o">=</span> <span class="n">total_value_has_become</span> <span class="o">-</span> <span class="n">total_value_was</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">portfolio_value</span> <span class="o">+=</span> <span class="n">delta</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">PortfolioStrategyV2</span> <span class="o">&lt;</span> <span class="no">AccursedDelegator</span>
  <span class="k">def</span> <span class="nf">update_portfolio_value</span>
    <span class="n">total_value_was</span><span class="p">,</span> <span class="n">total_value_has_become</span> <span class="o">=</span> <span class="n">attribute_was</span><span class="p">(</span><span class="ss">:total_value</span><span class="p">),</span> <span class="n">total_value</span>
    <span class="n">delta</span> <span class="o">=</span> <span class="n">total_value_has_become</span> <span class="o">-</span> <span class="n">total_value_was</span>
    <span class="n">tax</span> <span class="o">=</span> <span class="n">calculate_tax_over_potential_earnings</span><span class="p">(</span><span class="n">delta</span><span class="p">)</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">portfolio_value</span> <span class="o">+=</span> <span class="n">delta</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">potential_tax_due</span> <span class="o">+=</span> <span class="n">tax</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Note that thing such as this <code class="language-plaintext highlighter-rouge">AccursedDelegator</code> are bound to cause involuntary muscle contractions in developers who discover your code after you. It is a sharp knife, and using it merits an explanation. This is in alignment with my rule for “clever code” in general, which lauds as follows:</p>

<blockquote>
  <p>❗️ Clever code is almost never necessary, but when it is - your peers deserve an explanation as to why.</p>
</blockquote>

<h3 id="conclusion">Conclusion</h3>

<p>With a bit of work it is trivial to have versioned business logic, and Ruby allows us to do so with minimum changes to the logic itself. This is one of the cases where introducing indirection is a near-perfect tool for the job.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Every succesful application evolves. Business logic is often one of the things that evolves the most, but it is customary to have data which changes over time. Sometimes - over months or years. A lot of spots have logic related to “data over time”. For example: you collect payments from users, but some users were not getting charged VAT. Your new users need to get charged VAT, but they will also pay more, but you want to “grandfather” your existing users into a pricing plan where VAT is included in their pricing, so that the amount they pay does not change. ActiveRecord, by default, is not very conductive to such changes, but I have recently discovered a very nice pattern for adding versioned logic to models.]]></summary></entry><entry><title type="html">On the value of interfaces (and when you need one)</title><link href="https://blog.julik.nl/2024/02/interfaces-and-ddd-and-oh-my" rel="alternate" type="text/html" title="On the value of interfaces (and when you need one)" /><published>2024-02-15T00:00:00+00:00</published><updated>2024-02-15T00:00:00+00:00</updated><id>https://blog.julik.nl/2024/02/interfaces-and-ddd-and-oh-my</id><content type="html" xml:base="https://blog.julik.nl/2024/02/interfaces-and-ddd-and-oh-my"><![CDATA[<p>It is curious how people tend to bash DDD. I must admit - I never worked in a full-on DDD codebase or on a team that practices it, but looking at the mentioned articles <a href="https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice">like this one</a> does make me shudder a little. There is little worse than a premature abstraction, and a there is a noticeable jump (or rather: a trough) which goes from abstraction to indirection. I’ve been programming for more than 20 years now - 12 of those professionally <a href="https://www.imdb.com/name/nm3127224/">(with a little stint in-between)</a> and I also went from obsessing over abstractions to a more, let’s say, “common sense” approach to them. Oddly enough, this is not about OOP for me - it is about modules. And, to an extent, types - but I do believe types and behavior are going to stay connected in meaningful way. Whether you do <code class="language-plaintext highlighter-rouge">point.move()</code> or <code class="language-plaintext highlighter-rouge">move(point)</code> is not of importance as long as it generalizes over some kind of <code class="language-plaintext highlighter-rouge">Movable</code>.</p>

<p>It did take a while for a more digestible take on this to begin to crystallize, so I fugured it could be put on paper.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="solid-principles-are-not-as-much-about-objects-as-they-are-about-modules">SOLID principles are not as much about objects as they are about modules</h2>

<p>The key in “getting” the SOLID principles is that they allow modules to be swapped for one another, within reason. Abiding by those principles make modules easier to swap. Not more, not less. There could be a discussion about “X or Y makes things easier to reason about”, but I find the “reason about” take to get wheeled out when one wants to bash another’s programming paradigm, and we’re not here for that. What this is about applies in equal measure to both the super-strict languages like Idris and to the wildest dynlangs like Ruby.</p>

<p>The principles allow us to make software that composes better.</p>

<h2 id="dont-overdo-it">Don’t overdo it</h2>

<p>As in the example provided by <a href="https://blog.thelonearchitect.com/a-gentle-introduction-to-domain-driven-design-dc7cc169b1d">the lone architect</a> - let’s quote:</p>

<blockquote>
  <p>It’s fine for most simple use cases. It’s very readable, barely testable and it’s all in one place.
It just does the job. But there’s many caveats :</p>
</blockquote>

<p>Are there though? Is there anything else that needs to do things to the <code class="language-plaintext highlighter-rouge">users</code> table in the database? Why is this code “barely testable”? Is making a <code class="language-plaintext highlighter-rouge">DBConnection</code> available in the scope the function runs impossible? And of course it is perfectly testable - feed it a request and a response, make sure the request contains the two params, make sure the response got filled with a JS-object-ish thing having the properties <code class="language-plaintext highlighter-rouge">id</code> and <code class="language-plaintext highlighter-rouge">emailAddress</code>, make sure there is a row in the database. It just doesn’t need the modulization and the encapsulation and the interfacement smeared all over it in thick gobs. When it does become interesting to change it for the better though, is when (and only when!) additional requirements arrive - which are also preferably present not only within this function. For example:</p>

<ul>
  <li>What if we need to encrypt the password? (if there is anything wrong with this example function - this is it, fixing this would make it near-perfect)</li>
  <li>What if we need to be able to set the password from a commandline tool, not only from an API web request?</li>
  <li>What if we need to store our users someplace else (if we have decided to take a large contract with an auth-as-a-service-SAAS-company, <a href="https://www.bleepingcomputer.com/news/security/auth0-warns-that-some-source-code-repos-may-have-been-stolen/">because</a> <a href="https://www.okta.com/blog/2022/04/okta-concludes-its-investigation-into-the-january-2022-compromise/">those never</a> <a href="https://sec.okta.com/harfiles">have data breaches</a> - and are thus more secure than our DB</li>
</ul>

<p>Now thest two would be much better examples of what can change in this function. But if we need none of the above: “all in one place” is <em>priceless.</em> All in one place is easy to find, easy to read in one sitting, easy to scan for bugs, and you don’t have to jump between files or classes or what-have-you. <a href="https://htmx.org/essays/locality-of-behaviour/">All in one place is good. Do have things in one place.</a></p>

<h2 id="modules-compose-over-interfaces">Modules compose over interfaces</h2>

<p>Your modules are like an AC power outlet and your appliances, and they compose (integrate) over the interface of a power plug. It is that simple. The presence of an interface makes sense if, and only if, there are multiple modules on either end of the interface and those modules can be swapped for one another. No multiple modules = no interface, YAGNI, KISS, don’t do it.</p>

<h2 id="defining-interfaces">Defining interfaces</h2>

<p>But what if you <em>do</em> need to swap implementations? Well, then your modules will integrate (compose, plug, connect…) over some kind of interface.
Defining good interfaces is key. They should be small, so that they are easy to understand. An AC power plug is very easy to understand. They should not change without a big need to do so, or change within reason and allowing existing use cases. An AC power outlet with a ground socket is compatible with an AC power plug without a ground pin. Interfaces should be well-tested and well-communicated - a power outlet and a plug are well-specced items.</p>

<p>Coming from dynamic languages, I find that “enforcing” interfaces is very rarely justified. Every use case which is often excercised is going to reveal corner cases which are not covered by the existing interface. Instead of enforcing it (be it with language constructs like <code class="language-plaintext highlighter-rouge">final</code>, or with calling conventions like not allowing additional fields in a proto message or additional properties in a JSON object) define the minimum viable interface the caller needs to conform to. The smaller the interface, the easier it will be to conform to and to understand how it works. Even smallest interfaces will have corner cases -  the <code class="language-plaintext highlighter-rouge">Read()</code> call in the <code class="language-plaintext highlighter-rouge">io.Reader</code> interface in Go has ambiguous behavior (is EOF an error or a read of zero-size?). A big interface makes a perfect breeding ground for corner cases big and small (did anyone mention <a href="https://guides.rubyonrails.org/active_record_basics.html">ActiveRecord</a>?)</p>

<h2 id="plugs-and-sockets-all-the-way-down">Plugs and sockets, all the way down</h2>

<p>An interface is just that - it is a set of rules by which two modules can interact with each other. The most obvious and familiar interface is <em>the connection of the mains outlet</em> in your office or apartment. The power outlet on the wall is a <em>module</em>. The plug on the end of the mains cord of your fridge is another <em>module</em>. The way by which these two combine to make a connection is the <em>interface</em>. The interface specifies a whole lot of interesting details:</p>

<ul>
  <li>Whether the connection must pass earth or not (is there a pin that sticks out and disallows you to use non-grounded appliances?)</li>
  <li>That this is a single-phase connection</li>
  <li>That this connection is AC - if you can rotate your plug by 180 degrees and it will still work</li>
  <li>and many other small details</li>
</ul>

<p>Whether to have those <em>interfaces</em> (or <a href="https://en.wikipedia.org/wiki/Design_by_contract">contracts</a>) in your system is the decision you need to make.</p>

<p>My beef however is this: DDD creates interfaces between the internal components of your system. A lot, a lot of them. Most of them will never be used to their potential, because you will never swap a <code class="language-plaintext highlighter-rouge">UsersRepository</code> for an <code class="language-plaintext highlighter-rouge">Auth0Repository</code> - and if you do, you will likely find that your interface is sorely lacking. The problem with interfaces is the same as with any other kind of process - you can’t design them in a vacuum. Creating the power outlet and plug <a href="https://en.wikipedia.org/wiki/History_of_AC_power_plugs_and_sockets">took multiple iterations over multiple decades</a> - and most importantly, the need for swapping modules at both ends of the interface was always present. Creating an interface for just <em>this one</em> login page to talk to <em>just that one</em> <code class="language-plaintext highlighter-rouge">UsersRepository</code> is nothing of the sort.</p>

<h2 id="do-i-actually-need-these-layers-and-modules-and-interfaces-and-oh-my">Do I actually need these layers and modules and interfaces and oh my?</h2>

<p>So, hereby: rules of thumb for deciding whether you need an interface. I’ve followed this for quite a while, and they rarely failed me. At least when the climate was right.</p>

<p>Simple! Think about the answers to the following questions. You don’t have to tell me, because there might be a couple of spicy ones there. Just think for yourself. If most of the answers are “yes” - you might. If most of the answers are “no” - likely you won’t, or at least not in this stage of the system’ development (or - not in this stage of the development of your organization).</p>

<ul>
  <li>Will the modules/layers of the system have to be substituted for other implementations, right now? <em>Mains analogy:</em> yes, both the plug and the power outlet may be replaced. Any appliance might get plugged into the power outlet that is being worked on. An appliance being worked on may need to be plugged into a different power outlet.</li>
  <li>Is it so that right now different teams (especially teams that don’t see eye to eye, or have different incentives) will be working on different layers? Mains analogy: it is the manufacturer of the appliance who produces the appliance. And it is your local electrician who is responsible for adequately mounting the power outlet. They are different entities, they have little in common, and they are likely to have different incentives.</li>
  <li>Is it so that the modules/layers are going to be using different technology, or be developed in different programming languages? Mains analogy: well, this is a bit silly of course, but an appliance can be produced at a factory and sealed in epoxy, while a power outlet might be connected to 50-year old copper wire in your basement.</li>
  <li>Are there any strict standards a module needs to conform to, while other modules in the system would not? <em>Mains analogy:</em> a power outlet needs to conform to building code. And an appliance must conform to earth shielding, magnetic emission regulations, and any other legalities that apply.</li>
  <li>Will using an interface make using at least one of the collaborating modules vastly more convenient, right now? <em>Mains analogy:</em> you can eschew a power outlet and a plug, and just twist the naked copper wires together. It would totally work, but it would make things very inconvenient even for slightest changes - like moving an appliance or vacuuming the space around it.</li>
  <li>Is there a sizeable body of people (contractors, teammates, customers) who just cannot in their right mind use the system and not break it, unless the usage is guided by extremely strict, narrow constraints - and should therefore be “frozen” and documented? <em>Mains analogy:</em>  A power outlet is designed in a certain way so that toddlers would be less likely to stick their fingers in it, and it is helpful that all power outlets are the same.</li>
</ul>

<p>The more “yes”-s you have collected, the more reason you have to do layering/interfaces/modules in this particular scenario. If this made you doubt - the answer is likely a “no”. Don’t do modules, don’t do interfaces, don’t do <a href="https://www.joelonsoftware.com/2001/04/21/dont-let-architecture-astronauts-scare-you/">architecture astronautics.</a> Hexagonal, pentagrammatic or tetrahedric - just don’t.</p>

<p>Note that I am liberally sprinkling this with the words “right now”. If you do not need it now - you might very well need it in the future, but you will be designing your module and interface <em>then,</em> not now. By then, your system will have gestated and it is likely to scream at you where to introduce <a href="https://www.oreilly.com/library/view/unified-modeling-language/0321267974/ch11.html">seams.</a></p>

<p>Seriously: if you do not need it now - you do not need the hexagonal architecture, and you don’t need the Repository pattern, and you don’t need the bounded context, and you might not need the Clean Architecture.</p>

<h2 id="applying-the-questionnaire-to-avoid-premature-layering">Applying the questionnaire to avoid premature layering</h2>

<p>It is hard to slice a sandwich in thin slices - especially so if the sandwich is already thin. <a href="https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice#layers-in-ddd-microservices">This article</a> shows us a layering of a hypothetical <code class="language-plaintext highlighter-rouge">Ordering</code> microservice in an “Application layer”, “Domain model layer” and an “Infrastructure layer”. There is a number of alarms here suggesting this to be a questionable proposition. First: if your microservice needs 3 layers, is it really that “micro” to begin with? Second, let’s apply our questionnaire to that microservice.</p>

<ul>
  <li>❌ Will the modules/layers of the system have to be substituted for other implementations, right now? <em>Clearly not</em> - there will be just one implementation of persistence, only one implementation of domain logic and just one web API endpoint.</li>
  <li>❌ Are there any strict standards a module needs to conform to, while other modules in the system would not? <em>Clearly not</em> - this is a self-contained microservice</li>
  <li>❌ Is it so that right now different teams will be working on different layers? <em>No</em> because it is not specified - we will assume the Ordering Microservice is not getting massively hired for this quarter</li>
  <li>❌ Is it so that the modules/layers are going to be using different technology, or be developed in different programming languages? <em>No,</em> it is .NET all the way down</li>
  <li>❓ Will using an interface make using at least one of the collaborating modules vastly more convenient, right now? <em>No</em> as they all live inside this microservice and won’t be exported/exposed in any other way. One could say that it is probably good to separate the web request/response handling part and have some form of interfacting to the rest of the system, but no extreme need to do so.</li>
  <li>❓ Is there a sizeable body of people (contractors, teammates, customers) who just cannot in their right mind use the system and not break it? <em>Not quite</em> - this is not specified, and we will assume people editing the Ordering Microservice at least know .NET The consistency of web calls can be maintained with a simple schema validation of the input and well-tested output. <em>That</em> is the only interface this system deserves.</li>
</ul>

<p>Therefore: should the Ordering Microservice be layered? Judging from the article: <strong>hell no.</strong></p>

<p>Interfaces and layering are cool, but once you actually have use for them. In other situations there is likely an entity trying to sell you a bridge.</p>

<p>Or trying to sell you a digital transformation with a hefty consulting bill attached. Which is roughly the same thing.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[It is curious how people tend to bash DDD. I must admit - I never worked in a full-on DDD codebase or on a team that practices it, but looking at the mentioned articles like this one does make me shudder a little. There is little worse than a premature abstraction, and a there is a noticeable jump (or rather: a trough) which goes from abstraction to indirection. I’ve been programming for more than 20 years now - 12 of those professionally (with a little stint in-between) and I also went from obsessing over abstractions to a more, let’s say, “common sense” approach to them. Oddly enough, this is not about OOP for me - it is about modules. And, to an extent, types - but I do believe types and behavior are going to stay connected in meaningful way. Whether you do point.move() or move(point) is not of importance as long as it generalizes over some kind of Movable. It did take a while for a more digestible take on this to begin to crystallize, so I fugured it could be put on paper.]]></summary></entry><entry><title type="html">UI algorithms: drag-reordering</title><link href="https://blog.julik.nl/2022/10/drag-reordering" rel="alternate" type="text/html" title="UI algorithms: drag-reordering" /><published>2022-10-22T00:00:00+00:00</published><updated>2022-10-22T00:00:00+00:00</updated><id>https://blog.julik.nl/2022/10/drag-reordering</id><content type="html" xml:base="https://blog.julik.nl/2022/10/drag-reordering"><![CDATA[<p>A list where you can reorder items is one of entrenched widgets in UIs. Everyone knows how they are supposed to work, they are cheap to build, intuitive and handy. The problem is that they often get built wrong (not the “just grab Sortable.js and be done with it”-kind-of-wrong, but the “Sortable.js does not provide good user experience”-kind-of-wrong). I’ve built a couple of these for various projects and I believe there is an approach that works fairly nicely. So let’s build us a reorderable list with drag&amp;drop. As usual, we will be doing this <em>laaaive</em> without React and without any libraries.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>A few disclaimers before we get started:</p>

<ul>
  <li>The examples I give here are not accessible. This should be better.</li>
  <li>I am not discounting the effort libraries like <a href="https://github.com/SortableJS">Sortable.js</a> have put together, but I do believe they get some key aspects of the interaction wrong. And not only that, but as a result of their implementation (using slots as drop targets) the resulting interaction is <em>worse</em> for the end-user. That’s what ultimately motivated me to write this post.</li>
  <li>At the moment the examples in this article do not work on mobile, sorry for that. You can however extend everything described here to touch/pointer events.</li>
</ul>

<p>Let’s talk reordering. Like so – try drag-reordering some items in this list.</p>

<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="QWxbJrX" data-user="julik" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/julik/pen/QWxbJrX">
  Proximity-based reorder (vertical)</a> by Julik (<a href="https://codepen.io/julik">@julik</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

<p>Let’s note a few peculiarities of this behavior. First, many reordering interactions on the modern web give you a “preview” of where the element is going to be inserted, like so:</p>

<p><img src="/assets/2022-reordering/ghost-elem.png" alt="Ghost Elem" /></p>

<p>…whereby the elements after the one you are inserting are moving out of the way and the elements being relocated are shown at the spot they would be inserted in if the interaction succeeds. The displayed elements are called <code class="language-plaintext highlighter-rouge">ghost</code> elements, at least this is the SortableJS terminology and I would consider it canonical at this point. This is the most popular (and rightfully so) approach on mobile devices. Indeed, on mobile this makes some sense, as finger motion is usually pretty crude and interacting elements are large.</p>

<p>I vastly prefer the second approach though, where the position where the reordering elements will be inserted is indicated by an insertion marker:</p>

<p><img src="/assets/2022-reordering/insertion-cursor.png" alt="Insertion Cursor" /></p>

<p>This is a much better approach where the elements being reordered are small, where the pointing device is precise – like a pencil or a mouse. But most importantly: it is less frustrating for the person providing the input. When we use the first – let’s call it “mobile” - model of reordering, the user has to “aim” for a moving target - an insertion marker that travels. Once you aim for an insertion point, by the time your drag reaches it the element you wanted to insert before or after has shifted to give you the preview. You then realise you are making a mistake, and drag to a different spot - only to find it shift just under your cursor. This can be incredibly frustrating, and – frankly – even on mobile there is a case to be made for not shifting the receiving area underneath the drag interaction unnecessarily.</p>

<p>Since this is about the implementation of the interaction, it helps to analyse the <strong>expectations</strong> the user would have when doing the reorder. When we reorder, we near-always start with a mental model of “placing these items before that item” or “placing this item after that other one”. That is: there is always an item in our list, or gallery, or what have you - relative to which we want to finish our interaction. Developing a good reorder depends on using this key observation: <strong>a reorder is relative to one of the participating elements.</strong></p>

<p>To see how that is implemented in the list above, we can take a walkthrough of the algorithm.</p>

<p>When you start dragging, an <em>event handler</em> is installed for the <code class="language-plaintext highlighter-rouge">drag</code> event, which can supply the pointer coordinates as the mouse is moving. While it is moving, we continuously calculate the following two results:</p>

<ul>
  <li>What is the closest participating element (box) we are dragging next to?</li>
  <li>Is the user’s <em>likely intent</em> to place the items <em>before</em> or <em>after</em> that closest element?</li>
</ul>

<p>Take note of that second part! Many of reordering examples actually use existing items as <em>slots</em> onto which you drop your “traveling” item. <a href="https://code-boxx.com/drag-drop-sortable-list-javascript/">In this example</a> you will see the “receiving element” be used to show you where your dragged item would end up in the list. This is not a great idea, because “between” as well as “outside” of those slots there is no interaction cue at all! which is exactly what the user would want.</p>

<p>Here is something one should not do though: we should not try to do our detection in terms of “wanting to position this element <em>between</em> that element and that other element”. Why? Well, it makes one thing much less pleasant: placing the item at the end or at the start of our list. We’ll get there.</p>

<p>If this seems complex or intricate: it is. UI is hard.</p>

<h3 id="implementation-finding-the-closest-element">Implementation: finding the closest element</h3>

<p>We have two parts to this challenge thus: finding the closest element and determining the user intent. Let’s deal with the first one.</p>

<p>To figure out the closest element, we can do the simplest thing possible: calculate euclidean distances from the pointer to every participating element, and then sort them by those distances. The closest element is the one with the smallest distance to the pointer. Kind of like so:</p>

<p><img src="/assets/2022-reordering/shortest.png" alt="Proximity sort" /></p>

<p>Here the  the shortest euclidean distance is the one indicated by the solid line. This is an extremely robust method with a whole slew of advantages:</p>

<ul>
  <li>Unlike a “virtual line” which goes through the list, this approach works with items located in a gallery flow</li>
  <li>We only need to find one element relative to which the item is being placed</li>
  <li>For it to work the pointer does not have to be inside the closest element - or anywhere inside the participating elements for that matter!</li>
</ul>

<blockquote>
  <p>💡 If you want to make this interaction more sophisticated: centroids work great when your participating elements are roughly of the same size. If your elements are very different in size it is possible that an element which is much larger but closer to the pointer gets discarded and a smaller element which is closer gets picked instead. Should that happen - use 5 points for matching instead of just the centroid. Take the 4 corners of the box and the centroid for that, optionally raising to 9 with midpoints of the box edges. This will give much more accurate snapping at the cost of 5 or 9 times as many computations and a slower sort. But for most applications just using the centroid will likely be good enough.</p>
</blockquote>

<p>To compute centroids we will need the <code class="language-plaintext highlighter-rouge">getBoundingClientRect</code>:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">function</span> <span class="nf">computeCentroid</span><span class="p">(</span><span class="nx">element</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">rect</span> <span class="o">=</span> <span class="nx">element</span><span class="p">.</span><span class="nf">getBoundingClientRect</span><span class="p">();</span>
    <span class="kd">const</span> <span class="nx">viewportX</span> <span class="o">=</span> <span class="p">(</span><span class="nx">rect</span><span class="p">.</span><span class="nx">left</span> <span class="o">+</span> <span class="nx">rect</span><span class="p">.</span><span class="nx">right</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">viewportY</span> <span class="o">=</span> <span class="p">(</span><span class="nx">rect</span><span class="p">.</span><span class="nx">top</span> <span class="o">+</span> <span class="nx">rect</span><span class="p">.</span><span class="nx">bottom</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
    <span class="k">return</span> <span class="p">{</span><span class="na">x</span><span class="p">:</span> <span class="nx">viewportX</span> <span class="o">+</span> <span class="nb">window</span><span class="p">.</span><span class="nx">scrollX</span><span class="p">,</span> <span class="na">y</span><span class="p">:</span>  <span class="nx">viewportY</span> <span class="o">+</span> <span class="nb">window</span><span class="p">.</span><span class="nx">scrollY</span><span class="p">};</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>Note the addition of <code class="language-plaintext highlighter-rouge">window.scrollX</code> and <code class="language-plaintext highlighter-rouge">scrollY</code> here: these are important because we want <em>page</em> coordinates, not <em>viewport</em> coordinates. Like so:</p>

<p><img src="/assets/2022-reordering/page-viewport-coords.png" alt="Viewport coordinates versus page coordinates" /></p>

<p>This will become important later. For now we will store all our computed centroids - along with their corresponding elements - in an array:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">participatingElements</span> <span class="o">=</span> <span class="nb">Array</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">list</span><span class="p">.</span><span class="nf">querySelectorAll</span><span class="p">(</span><span class="dl">"</span><span class="s2">li</span><span class="dl">"</span><span class="p">)).</span><span class="nf">map</span><span class="p">((</span><span class="nx">element</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">{</span><span class="na">centroid</span><span class="p">:</span> <span class="nf">computeCentroid</span><span class="p">(</span><span class="nx">element</span><span class="p">),</span> <span class="nx">element</span><span class="p">};</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Then we will need a function which will tell us the distance to our pointer event. Remember I told you that this <code class="language-plaintext highlighter-rouge">scrollX</code> will become important later on?</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">pageDistanceBetweenPointerAndCentroid</span><span class="p">(</span><span class="nx">evt</span><span class="p">,</span> <span class="nx">centroid</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nb">Math</span><span class="p">.</span><span class="nf">hypot</span><span class="p">(</span><span class="nx">centroid</span><span class="p">.</span><span class="nx">x</span> <span class="o">-</span> <span class="p">(</span><span class="nx">evt</span><span class="p">.</span><span class="nx">clientX</span> <span class="o">+</span> <span class="nb">window</span><span class="p">.</span><span class="nx">scrollX</span><span class="p">),</span> <span class="nx">centroid</span><span class="p">.</span><span class="nx">y</span> <span class="o">-</span> <span class="p">(</span><span class="nx">evt</span><span class="p">.</span><span class="nx">clientY</span> <span class="o">+</span> <span class="nb">window</span><span class="p">.</span><span class="nx">scrollY</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Note that you may as well use just the squared distance which allows you to skip a square root - all we want is to <em>compare</em> distances. A square root of a larger number will be a larger number, and will sort the same. I leave it with a <code class="language-plaintext highlighter-rouge">hypot</code> for clarity.</p>

<p>The story with subtracting <code class="language-plaintext highlighter-rouge">scrollX</code> and <code class="language-plaintext highlighter-rouge">scrollY</code> is this: we <em>precompute</em> and cache our centroids. The <code class="language-plaintext highlighter-rouge">distanceSquaredBetweenCursorAndPoint</code> gets evaluated for every participating element when the pointer moves and an event gets dispatched. The <code class="language-plaintext highlighter-rouge">computeCentroid</code> calls are done once - at the start of interaction - and our list can be large, so it might take a while.</p>

<p>The crucial reason for doing it this way is that “poking at the DOM” (doing the <code class="language-plaintext highlighter-rouge">getBoundingClientRect</code>) can be expensive, and there are benefits to doing it only once - at the start of the interaction. Doing some JS squaring will be <em>very</em> fast though, and will not touch the DOM.</p>

<p>When we take the snapshot of the centroid coordinates we are at a certain scroll offset. <em>While we drag</em> we can use the mouse wheel / touch gesture to actually scroll the document! So you should be able to pick up the item you need to place elsewhere, <em>scroll the page</em>, have the insertion point end up <em>underneath your cursor</em> position and drop the item. This is an interaction which is almost always bodged in Electron apps, and until fairly recently it was bodged in Spotify which made reordering playlists excruciatingly painful. Don’t do this to your user! Therefore, we do not want the cursor coordinates in the <em>viewport</em> (which is what we get by default), but we want it in <em>page coordinates</em> instead:</p>

<p><img src="/assets/2022-reordering/page-viewport-coords-cursor.png" alt="Page and viewport pointer coordinates" /></p>

<p>…so that we can then compute our distance correctly:</p>

<p><img src="/assets/2022-reordering/page-viewport-coords-distance.png" alt="Distance between pointer and centroid" /></p>

<blockquote>
  <p>💡 Simplest thing in the world. But get it wrong at your peril: this is why users of Electron apps are often so frustrated. It’s the little things implemented wrong or incompletely.</p>
</blockquote>

<p>Now we can set up an event handler on our drag:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">list</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">dragover</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">evt</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">evt</span><span class="p">.</span><span class="nf">preventDefault</span><span class="p">();</span>
  
  <span class="kd">const</span> <span class="nx">byProximity</span> <span class="o">=</span> <span class="nx">participatingElements</span><span class="p">.</span><span class="nf">map</span><span class="p">((</span><span class="nx">pe</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span><span class="na">d</span><span class="p">:</span> <span class="nf">pageDistanceBetweenPointerAndCentroid</span><span class="p">(</span><span class="nx">evt</span><span class="p">,</span> <span class="nx">pe</span><span class="p">.</span><span class="nx">centroid</span><span class="p">),</span> <span class="p">...</span><span class="nx">pe</span><span class="p">};</span>
  <span class="p">}).</span><span class="nf">sort</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">a</span><span class="p">.</span><span class="nx">d</span> <span class="o">-</span> <span class="nx">b</span><span class="p">.</span><span class="nx">d</span><span class="p">);</span>

  <span class="kd">const</span> <span class="nx">closest</span> <span class="o">=</span> <span class="nx">byProximity</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">element</span><span class="p">;</span>

  <span class="c1">// For now - cheekily dispatch a custom event</span>
  <span class="kd">const</span> <span class="nx">event</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Event</span><span class="p">(</span><span class="dl">"</span><span class="s2">reorder:accepts</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span><span class="na">cancelable</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">bubbles</span><span class="p">:</span> <span class="kc">true</span><span class="p">});</span>
  <span class="nx">closest</span><span class="p">.</span><span class="nf">dispatchEvent</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>And… believe it or not but this ends the “proximity” part! It really is that simple.</p>

<h3 id="implementation-determining-beforeafter-intent">Implementation: determining before/after intent</h3>

<p>Now things are about to get interesting. We know that if the user is close to an element, they want to sort <em>relative to it.</em>  We also need to know “relative in what sense” – that is, do they want their new element <em>before</em> or <em>after</em> the one closest to interaction.</p>

<p>The naive approach would be to look at the <em>second</em> closest element and just decide that “it must be between this and that”. Like so:</p>

<p><img src="/assets/2022-reordering/intent-between-bad-1.png" alt="Detection of intent works when distance is between two elements" /></p>

<p>And it would work fine, except when it wouldn’t:</p>

<p><img src="/assets/2022-reordering/intent-between-bad-2.png" alt="Detection of intent fails when inserting outside the list" /></p>

<p>Here, the user clearly wants to place his <code class="language-plaintext highlighter-rouge">Almonds</code> at the top of the list. But our amazing algorithm will instead place his item between the first and the second element! It gets even better though. Let me introduce you to our friend…</p>

<h3 id="the-gallery">The gallery</h3>

<p>Try doing some reordering here:</p>

<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="OJEVrye" data-user="julik" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/julik/pen/OJEVrye">
  Proximity-based reorder (vertical)</a> by Julik (<a href="https://codepen.io/julik">@julik</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

<p>By a “gallery” I mean a flow-like layout of items which generally arrange in rows first, and columns second. In that case our algorithm for proximity sorting still works:</p>

<p><img src="/assets/2022-reordering/gallery-proximity.png" alt="Proximity sort in a zigzag layout" /></p>

<p>…but trying to grab the closest 2 items and say that we are sorting between them will give us items <code class="language-plaintext highlighter-rouge">2</code> and <code class="language-plaintext highlighter-rouge">4</code> which are not even adjacent in our list! And it gets even worse when we want to reposition the item to the start of the list or the end.</p>

<p><img src="/assets/2022-reordering/gallery-bad.png" alt="How calculating the position between elements breaks in a zigzag layout" /></p>

<p>Instead we want to determine intent <em>relative to just one element,</em> without considering its siblings.</p>

<p>For that we can use <em>binary space partitioning.</em> From the centroid of the closest element, we need to pull a line dividing the entire viewport into a “before zone” and an “after zone” of the screen space. If our cursor is in the “before” zone, the user intent is to insert things <em>before</em> that element, and vice versa. For a list item in a vertical list, the “before” zone will be everything “north” of the centroid, and “after” will be everything “south” of the centroid. Below, our cursor is in the “before” half-plane, so the user intent is to insert “prior to closest”:</p>

<p><img src="/assets/2022-reordering/intent-vertical.png" alt="Detecting before/after intent in a vertical list" /></p>

<p>In our “gallery” example we do the same, but we partition the coordinate plane vertically. Everything left of the centroid of the closest element will be “before”, everything right of the centroid will be “after”:</p>

<p><img src="/assets/2022-reordering/intent-horizontal.png" alt="Detecting before/after intent in a gallery layout" /></p>

<blockquote>
  <p>💡 Note that this is obviously left-to-right centric, you might want to swap this if you have an RTL gallery or a bottom-up list</p>
</blockquote>

<p>How do we determine which partitioning to use, given that it depends on the shape of our list of items? We could make it a config option and force the developer to choose, but it feels lazy. There is a fairly simple heuristic though which allows us to detect the direction automatically: we already computed the coordinates of our centroids when the drag started, right? So we can look at the <em>delta</em> between the coordinates of, say, the first two centroids, and determine the most likely list direction based on that:</p>

<p><img src="/assets/2022-reordering/direction-deltas.png" alt="Detecting list direction in screen coordinates" /></p>

<p>If <code class="language-plaintext highlighter-rouge">dx</code> is larger we assume (mostly) horizontal direction, if <code class="language-plaintext highlighter-rouge">dy</code> is larger we assume vertical direction. If we do not have enough information - for instance if our list consists of just one item we will need to take an arbitrary default.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kd">const</span> <span class="nx">DIRECTION_HORIZONTAL</span> <span class="o">=</span> <span class="nc">Symbol</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">DIRECTION_VERTICAL</span> <span class="o">=</span> <span class="nc">Symbol</span><span class="p">();</span>

<span class="kd">function</span> <span class="nf">predictDirection</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="p">{</span>
 <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">a</span> <span class="o">||</span> <span class="o">!</span><span class="nx">b</span><span class="p">)</span> <span class="k">return</span> <span class="nx">DIRECTION_HORIZONTAL</span><span class="p">;</span>
 <span class="kd">const</span> <span class="nx">dx</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nf">abs</span><span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nx">centroid</span><span class="p">.</span><span class="nx">x</span> <span class="o">-</span> <span class="nx">a</span><span class="p">.</span><span class="nx">centroid</span><span class="p">.</span><span class="nx">x</span><span class="p">);</span>
 <span class="kd">const</span> <span class="nx">dy</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nf">abs</span><span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nx">centroid</span><span class="p">.</span><span class="nx">y</span> <span class="o">-</span> <span class="nx">a</span><span class="p">.</span><span class="nx">centroid</span><span class="p">.</span><span class="nx">y</span><span class="p">);</span>
 <span class="k">return</span> <span class="nx">dx</span> <span class="o">&gt;</span> <span class="nx">dy</span> <span class="p">?</span> <span class="nx">DIRECTION_HORIZONTAL</span> <span class="p">:</span> <span class="nx">DIRECTION_VERTICAL</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">listDirection</span> <span class="o">=</span> <span class="nf">predictDirection</span><span class="p">(...</span><span class="nx">participatingElements</span><span class="p">);</span>
</code></pre></div></div>

<p>This can be done right after we compute the centroids and cached - unless you allow resizes during drag.</p>

<blockquote>
  <p>💡 If you want to be even more precise, you can do something similar to what Sortable.js does and compute the direction of the list
starting at your closest element. There is a challenge though, in that if you happen to end up on the last element in a row in a grid
layout, you need to scan at least two adjacent elements in both directions (“up the list” and “down the list”) to figure out the
dominant direction. I found just taking the first two elements works way better.</p>
</blockquote>

<p>And now we can finally compute our intent (note that we need to add the <code class="language-plaintext highlighter-rouge">scrollX</code> and <code class="language-plaintext highlighter-rouge">scrollY</code> to
the event coordinates, so that we will be comparing in page coordinates):</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">INTENT_BEFORE</span> <span class="o">=</span> <span class="nc">Symbol</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">INTENT_AFTER</span> <span class="o">=</span> <span class="nc">Symbol</span><span class="p">();</span>

<span class="kd">function</span> <span class="nf">intentFrom</span><span class="p">(</span><span class="nx">direction</span><span class="p">,</span> <span class="nx">evt</span><span class="p">,</span> <span class="nx">centroid</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">direction</span> <span class="o">===</span> <span class="nx">DIRECTION_HORIZONTAL</span><span class="p">)</span> <span class="p">{</span>
   <span class="k">return </span><span class="p">((</span><span class="nx">evt</span><span class="p">.</span><span class="nx">clientX</span> <span class="o">+</span> <span class="nb">window</span><span class="p">.</span><span class="nx">scrollX</span><span class="p">)</span> <span class="o">&lt;</span> <span class="nx">centroid</span><span class="p">.</span><span class="nx">x</span><span class="p">)</span> <span class="p">?</span> <span class="nx">INTENT_BEFORE</span> <span class="p">:</span> <span class="nx">INTENT_AFTER</span><span class="p">;</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
   <span class="k">return </span><span class="p">((</span><span class="nx">evt</span><span class="p">.</span><span class="nx">clientY</span> <span class="o">+</span> <span class="nb">window</span><span class="p">.</span><span class="nx">scrollY</span><span class="p">)</span> <span class="o">&lt;</span> <span class="nx">centroid</span><span class="p">.</span><span class="nx">y</span><span class="p">)</span> <span class="p">?</span> <span class="nx">INTENT_BEFORE</span> <span class="p">:</span> <span class="nx">INTENT_AFTER</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>and expand our event handler:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">listDirection</span> <span class="o">=</span> <span class="nf">predictDirection</span><span class="p">(...</span><span class="nx">participatingElements</span><span class="p">);</span>
<span class="nx">list</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">dragover</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">evt</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">evt</span><span class="p">.</span><span class="nf">preventDefault</span><span class="p">();</span>
  
  <span class="kd">const</span> <span class="nx">byProximity</span> <span class="o">=</span> <span class="nx">participatingElements</span><span class="p">.</span><span class="nf">map</span><span class="p">((</span><span class="nx">pe</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span><span class="na">ds</span><span class="p">:</span> <span class="nf">distanceSquaredBetweenCursorAndPoint</span><span class="p">(</span><span class="nx">evt</span><span class="p">,</span> <span class="nx">pe</span><span class="p">.</span><span class="nx">centroid</span><span class="p">),</span> <span class="p">...</span><span class="nx">pe</span><span class="p">};</span>
  <span class="p">}).</span><span class="nf">sort</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">a</span><span class="p">.</span><span class="nx">ds</span> <span class="o">-</span> <span class="nx">b</span><span class="p">.</span><span class="nx">ds</span><span class="p">);</span>

  <span class="kd">const</span> <span class="p">{</span><span class="nx">element</span><span class="p">,</span> <span class="nx">centroid</span><span class="p">}</span> <span class="o">=</span> <span class="nx">byProximity</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
  <span class="kd">const</span> <span class="nx">intent</span> <span class="o">=</span> <span class="nf">intentFrom</span><span class="p">(</span><span class="nx">listDirection</span><span class="p">,</span> <span class="nx">evt</span><span class="p">,</span> <span class="nx">centroid</span><span class="p">);</span>

  <span class="kd">const</span> <span class="nx">eventName</span> <span class="o">=</span> <span class="nx">intent</span> <span class="o">===</span> <span class="nx">INTENT_BEFORE</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">reinsert:before</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">reinsert:after</span><span class="dl">"</span><span class="p">;</span>
  <span class="c1">// For now - cheekily dispatch a custom event</span>
  <span class="kd">const</span> <span class="nx">event</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Event</span><span class="p">(</span><span class="nx">eventName</span><span class="p">,</span> <span class="p">{</span><span class="na">cancelable</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">bubbles</span><span class="p">:</span> <span class="kc">true</span><span class="p">});</span>
  <span class="nx">element</span><span class="p">.</span><span class="nf">dispatchEvent</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>The rest is mostly implementation details - once you have the information where to sort <em>to</em> the rest is mostly DOM manipulation (or React prop wrangling, whichever rocks your boat).</p>

<h3 id="more-on-inserting-against-a-single-element">More on inserting against a single element</h3>

<p>With a list layout (vertical or horizontal) we could very well use an “is the insertion point between A and B” approach. However, using a single element by proximity and inserting before or after it has another pleasant side-effect: we can display the insertion marker where it makes more sense. Consider the following setup:</p>

<p><img src="/assets/2022-reordering/before-after-gallery.png" alt="Why ordering against one element is better" /></p>

<p>Here we can change the placement of the insertion marker. While both of these setups represent the same list mutation (insert between items 2 and 3) for the user there is benefit to showing the insertion marker closer to the item they are dragging next to. This can be easily achieved with some CSS smarts.</p>

<p>Note that this is a very reasonable case of <a href="https://acko.net/blog/i-is-for-intent/">I is for Intent</a> as well! From the standpoint of <em>state</em> there is no difference where the new element will be inserted - it will be between elements 2 and 3. But from the standpoint of <em>intent</em> it is much better for the user to have visual feedback that the result of their actions will take place in proximity to the element they are hovering over. Seriously, this is a great article - you should go and read it if you haven’t already.</p>

<h3 id="what-happens-after-we-know-where-to-insert">What happens after we know where to insert?</h3>

<p>Well, that’s easy (or hard, depending on how many layers of JS framework are between you and the DOM). For example, we can add an insertion marker:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">marker</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">createElement</span><span class="p">(</span><span class="nx">participatingElements</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">element</span><span class="p">.</span><span class="nx">nodeName</span><span class="p">);</span>
<span class="nx">list</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">dragover</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">evt</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// *skip*</span>
  <span class="nx">marker</span><span class="p">.</span><span class="nf">remove</span><span class="p">();</span> <span class="c1">// remove the previous marker</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">intent</span> <span class="o">===</span> <span class="nx">INTENT_BEFORE</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">marker</span> <span class="o">=</span> <span class="nx">element</span><span class="p">.</span><span class="nf">insertAdjacentElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">beforebegin</span><span class="dl">'</span><span class="p">,</span> <span class="nx">marker</span><span class="p">)</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="nx">marker</span> <span class="o">=</span> <span class="nx">element</span><span class="p">.</span><span class="nf">insertAdjacentElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">afterend</span><span class="dl">'</span><span class="p">,</span> <span class="nx">marker</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Or style our element relative to which we are sorting:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">list</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">dragover</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">evt</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// ...</span>
  <span class="nx">participatingElements</span><span class="p">.</span><span class="nf">map</span><span class="p">((</span><span class="nx">pe</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">pe</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="dl">"</span><span class="s2">inserting-before</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">pe</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="dl">"</span><span class="s2">inserting-after</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">});</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">intent</span> <span class="o">===</span> <span class="nx">INTENT_BEFORE</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">element</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="dl">"</span><span class="s2">inserting-before</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="nx">element</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="dl">"</span><span class="s2">inserting-after</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<p>In general, you would also want to capture the <code class="language-plaintext highlighter-rouge">dragend</code> event and then do your repositioning:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">const</span> <span class="p">{</span><span class="nx">closest</span><span class="p">,</span> <span class="nx">intent</span><span class="p">}</span> <span class="o">=</span> <span class="nf">stop</span><span class="p">();</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">intent</span> <span class="o">===</span> <span class="nx">INTENT_BEFORE</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">closest</span><span class="p">.</span><span class="nf">insertAdjacentElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">beforebegin</span><span class="dl">'</span><span class="p">,</span> <span class="nx">li</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="nx">closest</span><span class="p">.</span><span class="nf">insertAdjacentElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">afterend</span><span class="dl">'</span><span class="p">,</span> <span class="nx">li</span><span class="p">);</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>since this finishes the interaction. I found it the easiest to do this using a setup which is similar to React hooks:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">siblings</span> <span class="o">=</span> <span class="nx">evt</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">parentNode</span><span class="p">.</span><span class="nf">querySelectorAll</span><span class="p">(</span><span class="dl">"</span><span class="s2">li</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">stop</span> <span class="o">=</span> <span class="nf">initiateReorder</span><span class="p">(</span><span class="nx">siblings</span><span class="p">);</span> <span class="c1">// sets up event handlers, computes centroids and direction</span>
<span class="nx">evt</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">dragend</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">evt</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">evt</span><span class="p">.</span><span class="nf">preventDefault</span><span class="p">();</span>
  <span class="kd">const</span> <span class="p">{</span><span class="nx">closest</span><span class="p">,</span> <span class="nx">intent</span><span class="p">}</span> <span class="o">=</span> <span class="nf">stop</span><span class="p">();</span> <span class="c1">// removes the dragover handler etc.</span>
<span class="p">});</span>
</code></pre></div></div>

<p>With React there would some dances involved but you would end up with a similar setup.</p>

<h3 id="getting-a-bit-more-formal">Getting a bit more formal</h3>

<p>Our reordering can again be expressed as a set of functions, and amazingly they have little to do with the DOM or CSS. If you are writing it in something like Elm this is your jam:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>type ElementWithCentroid = (DOMElement, CentroidCoords)
type Siblings = [ElementWithCentroid]
type Intent = Before | After
type Direction = LeftToRight | RightToLeft | TopDown | BottomUp
insertRef(Siblings, Direction, PointerCoords) =&gt; (DOMElement, Intent)
</code></pre></div></div>

<p>and you would evaluate your <code class="language-plaintext highlighter-rouge">insertRef</code> at every change of the cursor coordinates.</p>

<p>It is a pretty neat algorithm:</p>

<ul>
  <li>It gracefully handles insertion at the start or at the end of our list of elements.</li>
  <li>It captures user intent better by not requiring the user to drag inside the participating elements only.</li>
  <li>It gracefully deals with there being just one participating element.</li>
  <li>It can easily handle insertion of arbitrary objects - they do not have to come from the same list! they may as well be files dragged in from the operating system. Heck, you can even insert multiple elements! And combine it with the <a href="https://codepen.io/julik/pen/mddVJOK">multiselect</a> UI algo I have described earlier.</li>
  <li>It does lend itself well to caching as element coordinates can be precomputed.</li>
</ul>

<h3 id="possible-optimisations">Possible optimisations</h3>

<p>I have applied none in the examples here, but the following are certainly possible.</p>

<ul>
  <li>Use squared distances for comparison to avoid a square root per distance</li>
  <li>Match “pointer inside rect of element” instead of proximity for cases when elements have varying sizes, or do it on N closest elements only</li>
  <li>Cull away all the rects which are outside the viewport. This potentially could be done with an <a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">IntersectionObserver</a> but increases complexity dramatically, as the observer is asynchronous.</li>
  <li>Compare less distances. This is known as the <a href="https://stackoverflow.com/questions/3498968/find-the-nearest-dot-in-a-2d-space">closest-point-problem</a> and with a vertical list it can be done using binary search (did I already say binary search is one of my faves? yay binary search!) but it gets nasty with galleries since distances are not monotonically increasing away from the pointer as we progress through the list.</li>
  <li>Throttle the pointer events as they can be extremely frequent, just make sure you capture tail.</li>
  <li>Throttle / limit DOM restyles / node insertions, as you only need them to give visual clues to the user - not to support the algorithm.</li>
  <li>…for example, not restyling if your previously-detected refrence element and intent are the same as the current ones</li>
  <li>…or use a vDOM-something-something and have no implicit DOM twiddling</li>
</ul>

<h3 id="wrapping-up">Wrapping up</h3>

<p>UI algorithms are fascinating. Sadly, a ton of today’s web apps get “the little things” wrong simply because they have to be done very quickly, for cheap, and as soon as the minimum allowable version is shipped nobody cares anymore. But with a little TLC we can still have our nice things. Trig works. Thinking about the user works. Not getting coerced into the React view of “you may not touch the DOM” works. Have at it!</p>]]></content><author><name></name></author><summary type="html"><![CDATA[A list where you can reorder items is one of entrenched widgets in UIs. Everyone knows how they are supposed to work, they are cheap to build, intuitive and handy. The problem is that they often get built wrong (not the “just grab Sortable.js and be done with it”-kind-of-wrong, but the “Sortable.js does not provide good user experience”-kind-of-wrong). I’ve built a couple of these for various projects and I believe there is an approach that works fairly nicely. So let’s build us a reorderable list with drag&amp;drop. As usual, we will be doing this laaaive without React and without any libraries.]]></summary></entry><entry><title type="html">The unreasonable effectiveness of leaky buckets (and how to make one)</title><link href="https://blog.julik.nl/2022/08/the-unreasonable-effectiveness-of-leaky-buckets" rel="alternate" type="text/html" title="The unreasonable effectiveness of leaky buckets (and how to make one)" /><published>2022-08-23T00:00:00+00:00</published><updated>2022-08-23T00:00:00+00:00</updated><id>https://blog.julik.nl/2022/08/the-unreasonable-effectiveness-of-leaky-buckets</id><content type="html" xml:base="https://blog.julik.nl/2022/08/the-unreasonable-effectiveness-of-leaky-buckets"><![CDATA[<p>One of the joys of approaching the same problem multiple times is narrowing down on a number of solutions to specific problems which <em>damn work.</em> One of those are <a href="/2021/12/actually-creating-an-idempotency-keys-gem">idempotency keys</a> - the other are, undoubtedly, rate limiters based on the leaky bucket algorithm. That one algorithm truly blew my mind the first time <a href="https://wjwh.eu/">Wander</a> implemented it back at WeTransfer.</p>

<p>Normally when people start out with rate limiting, a naive implementation would look like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">WebRequest</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">at_time: </span><span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">)</span>
<span class="k">if</span> <span class="no">WebRequest</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">key: </span><span class="n">request_ip</span><span class="p">).</span><span class="nf">where</span><span class="p">(</span><span class="s2">"at_time BETWEEN ? AND ?"</span><span class="p">,</span> <span class="mi">1</span><span class="p">.</span><span class="nf">minute</span><span class="p">.</span><span class="nf">ago</span><span class="p">,</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">).</span><span class="nf">count</span> <span class="o">&gt;</span> <span class="n">rate_limit</span>
  <span class="k">raise</span> <span class="s2">"Throttled"</span>
<span class="k">else</span>
  <span class="no">WebRequest</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">key: </span><span class="n">request_ip</span><span class="p">,</span> <span class="ss">at_time: </span><span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This has a number of downsides. For one, it creates an incredible number of database rows - and if you are under a DOS attack it can easily tank your database’ performance this way. Second - there is a time gap between the <code class="language-plaintext highlighter-rouge">COUNT</code> query and the <code class="language-plaintext highlighter-rouge">INSERT</code> of the current request. If your attacker is aggressive enough, they can overflow your capacity between these two calls. And finally - after your throttling window lapses - you need to delete all those rows, since you sure don’t want to keep storing them forever!</p>

<p>The next step in this type of implementation is usually “rollups”, with their size depending on the throttling window. The idea of the rollups is to store a counter for all requests that occurred within a specific time interval, and then to calculate over the intervals included in the window. For example, with requests during 5 seconds, we could have a counter with a secondly granularity:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="no">WebRequest</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">key: </span><span class="n">request_ip</span><span class="p">).</span><span class="nf">where</span><span class="p">(</span><span class="s2">"second_window BETWEEN ? AND ?"</span><span class="p">,</span> <span class="mi">5</span><span class="p">.</span><span class="nf">seconds</span><span class="p">.</span><span class="nf">ago</span><span class="p">.</span><span class="nf">to_i</span><span class="p">,</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">.</span><span class="nf">to_i</span><span class="p">).</span><span class="nf">sum</span><span class="p">(</span><span class="ss">:reqs</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">rate_limit</span>
  <span class="k">raise</span> <span class="s2">"Throttled"</span>
<span class="k">else</span>
  <span class="no">WebRequest</span><span class="p">.</span><span class="nf">find_or_create_by</span><span class="p">(</span><span class="ss">key: </span><span class="n">request_ip</span><span class="p">,</span> <span class="ss">second_window: </span><span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">.</span><span class="nf">to_i</span><span class="p">).</span><span class="nf">increment</span><span class="p">(</span><span class="ss">:reqs</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This also has its downsides. For one, you need to have good granularity counters, meaning that the interval which you count (be it 1 second, 1 day or 1 hour) need to be cohesive with your throttling limit. You can of course choose to use intervals sized to your throttling window and sum up “current and previous”, but even in that case you will need 2 of those:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>|••••••• 5s ••T-----|----- 5s -----T•••|
</code></pre></div></div>

<p>There is also an issue with precision. If you look at the above figure, “window counters” would indicate that your rate limit has been consumed even though looking back 5 seconds from <em>now</em> you are not covering the entire 5 second window looking back, but just a portion of it. So if you sum the counter for the current interval and the last, the sum might be higher than the actual resource use for the last 5 seconds.</p>

<p>As a matter of fact, this is what early version of <a href="https://github.com/WeTransfer/prorate">prorate</a> did.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h2 id="bring-me-the-bucket">Bring me the bucket!</h2>

<p>A leaky bucket solves all of these issues, and the best is - you need only one record per bucket. Let’s recap how it works. A leaky bucket is a <em>function over time</em> - something we as web developers do not encounter very often, but it will be familiar to anyone who coded games or simulations. It is something like water in a pipe or charge of a capacitor modeled over time. Let’s plot a leaky bucket with a capacity of <strong>3</strong> and a leak rate of 1.5 per second:</p>

<p><img src="/assets/rate-limiters/leaky-bucket-graph.png" alt="Plot of a leaky bucket over time" /></p>

<p>The bucket starts from the empty state (level of 0) and continues in that empty state until 1 second. At 1 second the bucket gets filled up with 1 unit of work. The level of the bucket then decays linearly at the constant rate of 1.5/s until it reaches zero again. Note that it does not go negative. At 1.7s 2 units of work get added, and the level shoots up to 2, and then decays again at the same linear rate. At 2s another 1 unit of work gets added, and again the level shoots up but then starts to decay. Then at 2.3s again 2 units of work get added. Now the level overshoots the 3 - which is the capacity of the bucket, denoted by the dashed line. The capacity of the bucket is limited at 3, so we do not allow the bucket to overfill - instead, we register the spillover as an <em>event of the bucket filling up</em>, which we can register instantly (indicated by the dashed red line). At this stage we allow the bucket level to decay linearly again, but we can use the known fact that the bucket did overflow when we filled it up to generate side effects - for example, for blocking further requests/units of work. Then the bucket level decays back to 0, and at 6 seconds 3 units of work get put in - which again fills the bucket up, which generates another “bucket filled up” event.</p>

<p>When we want to represent it digitally, it is enough to keep track of one value - the <code class="language-plaintext highlighter-rouge">level</code> of the bucket - and we could of course do it over discrete increments (like a millisecond, or a microsecond). But the decay of the bucket is very easy to interpolate, which gives us an easy way to represent this state by recalculating the level at the moments when the bucket gets filled up with units of work. As long as we know what the last known level was and at what time, we can calculate the current level by subtracting the leak rate multiplied by the time delta from last level measurement. Consequently, we only need to store two pieces of data for the entire bucket: the last measured level and the time at which the level got measured.</p>

<h2 id="tracking-buckets-economically">Tracking buckets economically</h2>

<p>Formally, such a leaky bucket can best be modeled as a <a href="https://en.wikipedia.org/wiki/Discrete_time_and_continuous_time">continuous time function</a> because knowing the “fillups” of the bucket (amount and occurrence time) is enough to calculate the level of the bucket in-between those occurrences, with arbitrary precision (as long as our calculation comes after a known sample). So, now the interesting part: how do we actually code this model?</p>

<p>Imagine we have a bucket which fits a certain capacity of <code class="language-plaintext highlighter-rouge">capacity</code>. This bucket may contain <code class="language-plaintext highlighter-rouge">capacity</code> - and no more - of content. There is a hole in the bucket, which leaks <code class="language-plaintext highlighter-rouge">leak_rate</code> contents over 1 unit of time (say – a second). When a request comes in, we put 1 unit of contents into the bucket:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>level = level + 1
</code></pre></div></div>

<p>We then calculate how much has leaked out of the bucket since we last looked at the bucket state, and we store the time when we addressed this particular leaky bucket:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>delta_t = now - atime
level = level - (leak_rate * delta_t)
atime = now
</code></pre></div></div>

<p>Then we clamp our bucket to the allotted capacity - to know whether we just filled it up or not:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>level = clamp(0, level, capacity)
</code></pre></div></div>

<p>We can then determine what actions to take based on the reached value of <code class="language-plaintext highlighter-rouge">level</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bucket_full = level == capacity
</code></pre></div></div>

<p>For example, we can set an arbitrary <code class="language-plaintext highlighter-rouge">capacity</code> and block the client until the bucket leaks back to its empty state:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>time_to_empty = level / leak_rate
[429, {"Retry-After" =&gt; time_to_empty.ceil.to_s}, []]
</code></pre></div></div>

<p>Or we might want to block the client for an arbitrary amount of time instead:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>time_to_empty = 30
[429, {"Retry-After" =&gt; time_to_empty.to_s}, []]
</code></pre></div></div>

<h2 id="no-need-to-simulate-an-entire-system--just-one-cell-is-enough">No need to simulate an entire system – just one cell is enough</h2>

<p>We can model the leaky buckets as a discrete time function, which is what Brandur was describing in his article <a href="https://brandur.org/rate-limiting#leaky-bucket-downsides">here</a>, where we would need to “drip” out the buckets at fixed time intervals, like so:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
  <span class="kp">loop</span> <span class="k">do</span>
    <span class="nb">sleep</span> <span class="mf">0.1</span>
    <span class="vg">$leaky_buckets</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">bucket</span><span class="o">|</span>
      <span class="n">bucket</span><span class="p">.</span><span class="nf">leak_according_to_delta</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Indeed, this setup has a very big downside - the trick though is that it is absolutely unnecessary! It is enough to “drip out” a bucket when we access it, and only that particular bucket - not the others. In fact this is the beauty of Wander’s implementation in Prorate which has impressed me so much!</p>

<p>And make no mistake - maybe this very article lists some “downside” to something which turns out not to be true. <a href="https://en.wikipedia.org/wiki/Trust,_but_verify">Trust, but verify.</a></p>

<h2 id="you-dont-have-to-only-fillup-by-1">You don’t have to only fillup by 1</h2>

<p>Crucially, while the description of leaky bucket algorithms implies that the bucket contains “tokens” - which would be things like TCP packets - all we need for a working implementation is an amount expressed as a floating point number. This means that we can use the leaky buckets for much nicer things than throttling an HTTP request. Specifically, that <code class="language-plaintext highlighter-rouge">n</code> that we “top up” the bucket with can actually be any value, a representation of <em>cost.</em> For example, imagine you have a system where a user may create a very large number of records in your database by performing batch requests. The user can send any number of articles, in a payload like so:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[
  {"id": "713bd518-1bc1-468c-a1a3-7e940b2fba01", "title": "How to get promoted"}, url: "https://www.spakhm.com/p/how-to-get-promoted"},
  {"id": "7f63d791-3d05-4529-9bbe-4a1fca510a9e", "title": "People can read their manager's mind", url: "https://yosefk.com/blog/people-can-read-their-managers-mind.html"},
  ...
]
</code></pre></div></div>

<p>You know that more than 10000 <code class="language-plaintext highlighter-rouge">INSERT</code>ed rows will tank the performance of your database. But you also know that allowing large lists of articles to be inserted at once is an essential function of your software, and you can’t really limit the requests to create no more than, say, 10 articles at a time. You also do not want to consider all requests equal - a request creating 1 article “costs” you less than a request which creates 1000 articles. With a leaky bucket we can actually transform the amount of <code class="language-plaintext highlighter-rouge">INSERT</code>s the request will do into the cost of a request, and use that instead of incrementing by 1:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">articles</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">request_body</span><span class="p">)</span>
<span class="n">n_rows</span> <span class="o">=</span> <span class="n">articles</span><span class="p">.</span><span class="nf">length</span>
<span class="n">level</span> <span class="o">=</span> <span class="n">clamp</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">level</span> <span class="o">-</span> <span class="p">(</span><span class="n">leak_rate</span> <span class="o">*</span> <span class="n">delta_t</span><span class="p">)</span> <span class="o">+</span> <span class="n">n_rows</span><span class="p">,</span> <span class="n">capacity</span><span class="p">)</span>
<span class="k">if</span> <span class="n">level</span> <span class="o">==</span> <span class="n">capacity</span>
  <span class="k">return</span> <span class="p">[</span><span class="mi">429</span><span class="p">,</span> <span class="p">{</span><span class="s2">"Retry-After"</span> <span class="o">=&gt;</span> <span class="n">time_to_empty</span><span class="p">.</span><span class="nf">to_s</span><span class="p">},</span> <span class="p">[</span><span class="s2">"Too many entries at once"</span><span class="p">]]</span>
<span class="k">end</span>  
</code></pre></div></div>

<p>By the same token you could assign a “cost” to an operation on your system and throttle on that.</p>

<h2 id="enough-babble-lets-build-one-for-ourselves">Enough babble, let’s build one for ourselves.</h2>

<p>A leaky bucket limiter is very straightforward, the only tricky bits are atomicity and how to make this atomicity function in combination with various data storage models. Let’s start with the simplest one: we will have an in-memory leaky bucket which only exists as long as the object owning it exists.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">LeakyBucket</span>
  <span class="k">class</span> <span class="nc">State</span>
    <span class="nb">attr_reader</span> <span class="ss">:level</span>

    <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">level</span><span class="p">,</span> <span class="n">is_full</span><span class="p">)</span>
      <span class="vi">@level</span> <span class="o">=</span> <span class="n">level</span>
      <span class="vi">@full</span> <span class="o">=</span> <span class="n">is_full</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">full?</span>
      <span class="vi">@full</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">leak_rate</span><span class="p">:,</span> <span class="n">capacity</span><span class="p">:)</span>
    <span class="vi">@atime</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_MONOTONIC</span><span class="p">)</span>
    <span class="vi">@level</span> <span class="o">=</span> <span class="mf">0.0</span>
    <span class="vi">@leak_rate</span> <span class="o">=</span> <span class="n">leak_rate</span><span class="p">.</span><span class="nf">to_f</span>
    <span class="vi">@capacity</span> <span class="o">=</span> <span class="n">capacity</span><span class="p">.</span><span class="nf">to_f</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">fillup</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>
    <span class="n">now</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_MONOTONIC</span><span class="p">)</span>
    <span class="vi">@level</span> <span class="o">=</span> <span class="p">[</span><span class="mf">0.0</span><span class="p">,</span> <span class="vi">@level</span> <span class="o">-</span> <span class="p">((</span><span class="n">now</span> <span class="o">-</span> <span class="vi">@atime</span><span class="p">)</span> <span class="o">*</span> <span class="vi">@leak_rate</span><span class="p">)].</span><span class="nf">max</span>
    <span class="vi">@level</span> <span class="o">=</span> <span class="p">[</span><span class="vi">@level</span> <span class="o">+</span> <span class="n">n</span><span class="p">,</span> <span class="vi">@capacity</span><span class="p">].</span><span class="nf">min</span>
    <span class="vi">@atime</span> <span class="o">=</span> <span class="n">now</span>
    <span class="no">State</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="vi">@level</span><span class="p">,</span> <span class="vi">@level</span><span class="p">.</span><span class="nf">round</span> <span class="o">==</span> <span class="vi">@capacity</span><span class="p">.</span><span class="nf">round</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>You might wonder why we put <code class="language-plaintext highlighter-rouge">leak_rate</code> and <code class="language-plaintext highlighter-rouge">capacity</code> in the constructor – this will become clear in a minute. Let’s use our freshly obtained leaky bucket powers:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="mi">77</span><span class="p">]</span> <span class="n">pry</span><span class="p">(</span><span class="n">main</span><span class="p">)</span><span class="o">&gt;</span> <span class="n">b</span> <span class="o">=</span> <span class="no">LeakyBucket</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">leak_rate: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">capacity: </span><span class="mi">10</span><span class="p">)</span> <span class="c1"># Allow 10 requests in 10 seconds</span>
<span class="o">=&gt;</span> <span class="c1">#&lt;LeakyBucket:0x0000000110838978 @atime=3165329.044493, @capacity=10.0, @leak_rate=1.0, @level=0.0&gt;</span>
<span class="p">[</span><span class="mi">78</span><span class="p">]</span> <span class="n">pry</span><span class="p">(</span><span class="n">main</span><span class="p">)</span><span class="o">&gt;</span> <span class="n">b</span><span class="p">.</span><span class="nf">fillup</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="c1">#&lt;LeakyBucket::State:0x0000000102a62360 @full=false, @level=2.0&gt;</span>
<span class="p">[</span><span class="mi">79</span><span class="p">]</span> <span class="n">pry</span><span class="p">(</span><span class="n">main</span><span class="p">)</span><span class="o">&gt;</span> <span class="n">b</span><span class="p">.</span><span class="nf">fillup</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="c1">#&lt;LeakyBucket::State:0x0000000106706a00 @full=false, @level=2.776740999892354&gt;</span>
<span class="p">[</span><span class="mi">80</span><span class="p">]</span> <span class="n">pry</span><span class="p">(</span><span class="n">main</span><span class="p">)</span><span class="o">&gt;</span> <span class="n">b</span><span class="p">.</span><span class="nf">fillup</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="c1">#&lt;LeakyBucket::State:0x0000000106795cf0 @full=false, @level=3.899269000161439&gt;</span>
</code></pre></div></div>

<p>Note that we do not add a <code class="language-plaintext highlighter-rouge">full?</code> method to the <code class="language-plaintext highlighter-rouge">LeakyBucket</code> itself. Why is that? Well, if we look at our level plot figure, we will see that we can only register that the bucket reached it’s capacity momentarily, exactly at the moment of fillup. Using our model we can measure the level accurately after the capacity was hit, but by that time some level would have leaked out already! So while we can add a <code class="language-plaintext highlighter-rouge">level</code> method to the bucket, it will never actually indicate that the bucket is full (barring some time resolution gymnastics of the CPU and memory and whatnot). So this is certainly possible:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">b</span><span class="p">.</span><span class="nf">fillup</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span> <span class="c1">#=&gt; Reaches capacity</span>
<span class="n">b</span><span class="p">.</span><span class="nf">full?</span> <span class="c1">#=&gt; Likely to return false even though we reached capacity right before</span>
</code></pre></div></div>
<blockquote>
  <p>💡 We must return a <code class="language-plaintext highlighter-rouge">State</code> struct and use that for our check whether we filled up the bucket. That is because the bucket is only full for an instant.</p>
</blockquote>

<p>Also note that I <code class="language-plaintext highlighter-rouge">round</code> the level and capacity before comparing them - such is the tradeoff of using floats for the leaky bucket level. You could use the same model with rounded-off level, at which point your bucket would become “a bucket of tokens”.</p>

<p>It doesn’t seem like much - we have an in-memory state and if we are using it inside a web request we won’t be able to get a lot of use out of it. But even in this minimal form this leaky bucket can be used inside of a background job:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>b = LeakyBucket.new(leak_rate: 10, capacity: 30) # 300 requests during 30 seconds
loop do
  if b.fillup(1).full?
    sleep 1 # Allow some time to release
    next
  end

  remote_api_client.request("/payments", params)
end
</code></pre></div></div>

<p>We can control a request rate to an API and <code class="language-plaintext highlighter-rouge">sleep</code> if the requests are coming in too often.</p>

<h2 id="adding-rudimentary-flow-control">Adding rudimentary flow control</h2>

<p>Another interesting use of a leaky bucket is monitoring spending. Imagine you are allowed to spend no more than 1000.- AMUs (Arbitrary Money Units) on some type of expense within 30 days, since that is your budget - this is a need that could easily arise in a fintech for example. You could say that:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">spend_limit</span> <span class="o">=</span> <span class="no">LeakyBucket</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">leak_rate: </span><span class="p">(</span><span class="mf">1000.0</span> <span class="o">/</span> <span class="mi">30</span> <span class="o">/</span> <span class="mi">24</span> <span class="o">/</span> <span class="mi">60</span> <span class="o">/</span> <span class="mi">60</span><span class="p">),</span> <span class="ss">capacity: </span><span class="mi">1000</span><span class="p">)</span>
<span class="n">spend_limit</span><span class="p">.</span><span class="nf">fillup</span><span class="p">(</span><span class="mi">30</span><span class="p">)</span> <span class="c1"># Spent 30.- AMUs on groceries</span>
<span class="n">spend_limit</span><span class="p">.</span><span class="nf">fillup</span><span class="p">(</span><span class="mi">990</span><span class="p">).</span><span class="nf">full?</span> <span class="c1"># Overspent!</span>
</code></pre></div></div>

<p>In our example we do see that there is a feature one would likely want to have though. Imagine we want to determine whether we are permitted to spend a certain amount of money, and forbid that spend if it would be too large? If we use <code class="language-plaintext highlighter-rouge">fillup</code>, we would change the state of the bucket - while other transactions would need to go through unscathed, yet we already filled the bucket to the brim. For rate limiting this might be an acceptable shortcoming, but for spend this might be undesirable. It can be very useful to have a simple question method for this instead:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">able_to_accept?</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>
  <span class="n">now</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_MONOTONIC</span><span class="p">)</span>
  <span class="n">level_after_leak</span> <span class="o">=</span> <span class="p">[</span><span class="mf">0.0</span><span class="p">,</span> <span class="vi">@level</span> <span class="o">-</span> <span class="p">((</span><span class="n">now</span> <span class="o">-</span> <span class="vi">@atime</span><span class="p">)</span> <span class="o">*</span> <span class="vi">@leak_rate</span><span class="p">)].</span><span class="nf">max</span>
  <span class="n">level_after_leak</span> <span class="o">+</span> <span class="n">n</span> <span class="o">&gt;</span> <span class="vi">@capacity</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now, if our withdrawal would be too large, we can deny it and allow smaller withdrawals to go through:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">spend_limit</span> <span class="o">=</span> <span class="no">LeakyBucket</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">leak_rate: </span><span class="p">(</span><span class="mf">1000.0</span> <span class="o">/</span> <span class="mi">30</span> <span class="o">/</span> <span class="mi">24</span> <span class="o">/</span> <span class="mi">60</span> <span class="o">/</span> <span class="mi">60</span><span class="p">),</span> <span class="ss">capacity: </span><span class="mi">1000</span><span class="p">)</span>
<span class="k">if</span> <span class="n">spend_limit</span><span class="p">.</span><span class="nf">able_to_accept?</span><span class="p">(</span><span class="mi">30</span><span class="p">)</span>
  <span class="n">spend_limit</span><span class="p">.</span><span class="nf">fillup</span><span class="p">(</span><span class="mi">30</span><span class="p">)</span> <span class="c1"># Spend 30.- AMUs on groceries</span>
<span class="k">end</span>
<span class="k">if</span> <span class="n">spend_limit</span><span class="p">.</span><span class="nf">able_to_accept?</span><span class="p">(</span><span class="mi">990</span><span class="p">)</span>
  <span class="n">spend_limit</span><span class="p">.</span><span class="nf">fillup</span><span class="p">(</span><span class="mi">990</span><span class="p">)</span> <span class="c1"># Only spend what we are allowed to</span>
<span class="k">end</span>
</code></pre></div></div>

<blockquote>
  <p>💡 Note that this type of function often gets called <code class="language-plaintext highlighter-rouge">peek</code>, which is a very obscure name. The only positive of it is some chance of cognitive familiarity through tradition, yet that tradition largely belongs to mostly-retired people with a standing shortage of characters. I implore you to not call your functions <code class="language-plaintext highlighter-rouge">peek</code> unless they have to do with IO or parsers. Pretty pretty please!</p>
</blockquote>

<h2 id="sharing-a-leaky-bucket-between-requests">Sharing a leaky bucket between requests</h2>

<p>A leaky bucket which can only be used within one unit of work (a job or a web request) is not much to write home about - rate limiting truly comes into its own when used across requests. This is fairly easy to do, as long as we remember that our webserver (like Puma) will be multithreaded, so the accesses to a particular bucket should be mutexed-protected. We will need a mutex for the shared leaky buckets table, and a mutex for every bucket we use. Thanks to the magic of OOP we can compose our existing <code class="language-plaintext highlighter-rouge">LeakyBucket</code> implementation into our expanded solution:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="vg">$leaky_buckets</span> <span class="o">=</span> <span class="p">{}</span>
<span class="vg">$leaky_buckets_mutex</span> <span class="o">=</span> <span class="no">Mutex</span><span class="p">.</span><span class="nf">new</span>

<span class="k">class</span> <span class="nc">SharedLeakyBucket</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">new</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="o">**</span><span class="n">options_for_leaky_bucket</span><span class="p">)</span>
    <span class="vg">$leaky_buckets_mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
      <span class="vg">$leaky_buckets</span><span class="p">[</span><span class="nb">name</span><span class="p">]</span> <span class="o">||=</span> <span class="k">super</span><span class="p">(</span><span class="o">**</span><span class="n">options_for_leaky_bucket</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="o">**</span><span class="n">options_for_leaky_bucket</span><span class="p">)</span>
    <span class="vi">@leaky_bucket</span> <span class="o">=</span> <span class="no">LeakyBucket</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="o">**</span><span class="n">options_for_leaky_bucket</span><span class="p">)</span>
    <span class="vi">@mutex</span> <span class="o">=</span> <span class="no">Mutex</span><span class="p">.</span><span class="nf">new</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">fillup</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>
    <span class="vi">@mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="p">{</span> <span class="vi">@leaky_bucket</span><span class="p">.</span><span class="nf">fillup</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This gives us a shared table of leaky buckets which is protected by a mutex, and that “singleton” mutex is only locked when we create or lookup our named bucket. The table is going to be keyed by the bucket name.</p>

<p>Now we can use our leaky bucket from multiple requests, as long as it has a descriptive <code class="language-plaintext highlighter-rouge">name</code> - such as the IP address that is accessing our service:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rack_app</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">req</span> <span class="o">=</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Request</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
  <span class="n">b</span> <span class="o">=</span> <span class="no">SharedLeakyBucket</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"throttle-</span><span class="si">#{</span><span class="n">req</span><span class="p">.</span><span class="nf">ip</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="ss">leak_rate: </span><span class="mi">10</span><span class="p">,</span> <span class="ss">capacity: </span><span class="mi">300</span><span class="p">)</span>
  <span class="k">if</span> <span class="k">if</span> <span class="n">b</span><span class="p">.</span><span class="nf">fillup</span><span class="p">(</span><span class="mi">1</span><span class="p">).</span><span class="nf">full?</span>
    <span class="p">[</span><span class="mi">429</span><span class="p">,</span> <span class="p">{</span><span class="s2">"Retry-After"</span> <span class="o">=&gt;</span> <span class="s2">"30"</span><span class="p">},</span> <span class="p">[]]</span> 
  <span class="k">else</span>
    <span class="c1"># ... do web stuffs</span>
  <span class="k">end</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If your requirements do not need sharing the leaky bucket between processes - say, you are running a small service from just one Puma process or using <a href="https://github.com/socketry/falcon">falcon</a> - and you are fine with the leaky buckets getting lost when the server is restarted - this can be fairly usable. Let’s not forget to clear out the old buckets from time to time:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="vg">$leaky_buckets_mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
  <span class="vg">$leaky_buckets</span><span class="p">.</span><span class="nf">delete_if</span> <span class="p">{</span> <span class="o">|</span><span class="n">_</span><span class="p">,</span> <span class="n">bucket</span><span class="o">|</span> <span class="n">bucket</span><span class="p">.</span><span class="nf">fillup</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nf">level</span> <span class="o">&lt;</span> <span class="mf">0.0001</span> <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>For example you can do this on every Nth call to <code class="language-plaintext highlighter-rouge">SharedLeakyBucket.new</code></p>

<h2 id="placing-the-bucket-in-a-database">Placing the bucket in a database</h2>

<p>While a database is not necessarily the best data store for leaky buckets because of very frequent writes it does offer us some advantages because of its intrinsic properties:</p>

<ul>
  <li>It is atomic, so no need for mutexes</li>
  <li>It can be shared between multiple processes and multiple servers</li>
</ul>

<p>Let’s implement our leaky bucket in terms of PostgreSQL. First, we create a table:</p>

<pre><code class="language-SQL">CREATE TABLE leaky_buckets (
  name VARCHAR(255) NOT NULL,
  level FLOAT DEFAULT 0.0,
  atime TIMESTAMP,
  expires_at TIMESTAMP
);
CREATE UNIQUE INDEX name_idx ON leaky_buckets (name);
CREATE INDEX exp_idx ON leaky_buckets (expires_at);
</code></pre>

<p>Then we need to reformat our <code class="language-plaintext highlighter-rouge">fillup</code> function to perform a SQL “upsert”, returning the newly reached level. Note that all the calculations are done in SQL so that the changes to the level are atomic and we do not end up with compare-and-set data races, this is what makes the SQL statement longer than one would expect:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">fillup</span><span class="p">(</span><span class="n">n_tokens</span><span class="p">)</span>
  <span class="n">conn</span> <span class="o">=</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection</span>

  <span class="c1"># Take double the time it takes the bucket to empty under normal circumstances</span>
  <span class="c1"># until the bucket may be deleted.</span>
  <span class="n">may_be_deleted_after_seconds</span> <span class="o">=</span> <span class="p">(</span><span class="vi">@capacity</span><span class="p">.</span><span class="nf">to_f</span> <span class="o">/</span> <span class="vi">@leak_rate</span><span class="p">.</span><span class="nf">to_f</span><span class="p">)</span> <span class="o">*</span> <span class="mf">2.0</span>

  <span class="c1"># Create the leaky bucket if it does not exist, and update</span>
  <span class="c1"># to the new level, taking the leak rate into account - if the bucket exists.</span>
  <span class="n">query_params</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">name: </span><span class="vi">@key</span><span class="p">,</span>
    <span class="ss">capa: </span><span class="vi">@capacity</span><span class="p">.</span><span class="nf">to_f</span><span class="p">,</span>
    <span class="ss">delete_after_s: </span><span class="n">may_be_deleted_after_seconds</span><span class="p">,</span>
    <span class="ss">leak_rate: </span><span class="vi">@leak_rate</span><span class="p">.</span><span class="nf">to_f</span><span class="p">,</span>
    <span class="ss">fillup: </span><span class="n">n_tokens</span><span class="p">.</span><span class="nf">to_f</span>
  <span class="p">}</span>
  <span class="n">sql</span> <span class="o">=</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">sanitize_sql_array</span><span class="p">([</span><span class="o">&lt;&lt;~</span><span class="no">SQL</span><span class="p">,</span> <span class="n">query_params</span><span class="p">])</span><span class="sh">
    INSERT INTO leaky_buckets AS t
      (name, atime, expires_at, level)
    VALUES
      (
        :name,
        clock_timestamp(),
        clock_timestamp() + ':delete_after_s second'::interval,
        LEAST(:capa, :fillup)
      )
    ON CONFLICT (key) DO UPDATE SET
      atime = EXCLUDED.atime,
      expires_at = EXCLUDED.may_be_deleted_after,
      level = GREATEST(
          0.0, LEAST(
            :capa,
            t.level + :fillup - (EXTRACT(EPOCH FROM (EXCLUDED.atime - t.atime)) * :leak_rate)
          )
        )
    RETURNING level
</span><span class="no">  SQL</span>

  <span class="c1"># Note the use of .uncached here. The AR query cache will actually see our</span>
  <span class="c1"># query as a repeat (since we use "select_value" for the RETURNING bit) and will not call into Postgres</span>
  <span class="c1"># correctly, thus the clock_timestamp() value would be frozen between calls. We don't want that here.</span>
  <span class="c1"># See https://stackoverflow.com/questions/73184531/why-would-postgres-clock-timestamp-freeze-inside-a-rails-unit-test</span>
  <span class="n">level_after_fillup</span> <span class="o">=</span> <span class="n">conn</span><span class="p">.</span><span class="nf">uncached</span> <span class="p">{</span> <span class="n">conn</span><span class="p">.</span><span class="nf">select_value</span><span class="p">(</span><span class="n">sql</span><span class="p">)</span> <span class="p">}</span>

  <span class="no">State</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">level_after_fillup</span><span class="p">,</span> <span class="p">(</span><span class="vi">@capacity</span> <span class="o">-</span> <span class="n">level_after_fillup</span><span class="p">).</span><span class="nf">abs</span> <span class="o">&lt;</span> <span class="mf">0.01</span><span class="p">).</span><span class="nf">tap</span> <span class="k">do</span>
    <span class="c1"># Prune buckets which are no longer used. No "uncached" needed here since we are using "execute"</span>
    <span class="n">conn</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="s2">"DELETE FROM leaky_buckets WHERE expires_at &lt; clock_timestamp()"</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>and similar for <code class="language-plaintext highlighter-rouge">able_to_accept?</code> like so:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">able_to_accept?</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>
  <span class="n">conn</span> <span class="o">=</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection</span>
  <span class="n">query_params</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">name: </span><span class="vi">@name</span><span class="p">,</span>
    <span class="ss">capa: </span><span class="vi">@capacity</span><span class="p">.</span><span class="nf">to_f</span><span class="p">,</span>
    <span class="ss">leak_rate: </span><span class="vi">@leak_rate</span><span class="p">.</span><span class="nf">to_f</span>
  <span class="p">}</span>
  <span class="c1"># The `level` of the bucket is what got stored at `atime` time, and we can</span>
  <span class="c1"># extrapolate from it to see how many tokens have leaked out since `atime` -</span>
  <span class="c1"># we don't need to UPDATE the value in the bucket here</span>
  <span class="n">sql</span> <span class="o">=</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">sanitize_sql_array</span><span class="p">([</span><span class="o">&lt;&lt;~</span><span class="no">SQL</span><span class="p">,</span> <span class="n">query_params</span><span class="p">])</span><span class="sh">
    SELECT
      GREATEST(
        0.0, LEAST(
          :capa,
          t.level - (EXTRACT(EPOCH FROM (clock_timestamp() - t.atime)) * :leak_rate)
        )
      )
    FROM 
      rate_limiter_leaky_buckets AS t
    WHERE
      name = :name
</span><span class="no">  SQL</span>

  <span class="c1"># If the return value of the query is a NULL it means no such bucket exists, so we assume the bucket is empty</span>
  <span class="n">current_level</span> <span class="o">=</span> <span class="n">conn</span><span class="p">.</span><span class="nf">uncached</span> <span class="p">{</span> <span class="n">conn</span><span class="p">.</span><span class="nf">select_value</span><span class="p">(</span><span class="n">sql</span><span class="p">)</span> <span class="p">}.</span><span class="nf">to_f</span>
  <span class="n">current_level</span> <span class="o">+</span> <span class="n">n</span> <span class="o">&gt;</span> <span class="vi">@capacity</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Note that we are adding an index and deleting expired buckets to keep our table to a reasonable size.</p>

<h2 id="placing-the-bucket-in-redis">Placing the bucket in Redis</h2>

<p>This will be long-winded, so really - just use <a href="https://github.com/WeTransfer/prorate">prorate.</a> Wander has described the basics of Lua scripting in Redis which makes Prorate tick in <a href="https://wjwh.eu/posts/2020-06-15-redis-eval-trick.html">his article</a> and the Lua code from Prorate can easily be loaded into Redis from something else than Ruby. Wander even built a small Prorate clone <a href="https://github.com/WJWH/prorate-cr">in Crystal.</a> Alternatively, if you are using <a href="https://sidekiq.org/products/pro.html">Sidekiq Pro</a> I’ve heard that it includes a leaky bucket rate limiter as well.</p>

<h2 id="in-closing">In closing</h2>

<p>There is of course more to explore with leaky buckets. For example one could add <a href="https://brandur.org/fragments/reservation-api">reservations</a> and all that, but this would make the post even longer than it already is. Let’s just finish off by saying that leaky buckets are great (and cheap), you can and possibly should add them to your application, and hopefully this article gives you a good general idea how to implement one.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[One of the joys of approaching the same problem multiple times is narrowing down on a number of solutions to specific problems which damn work. One of those are idempotency keys - the other are, undoubtedly, rate limiters based on the leaky bucket algorithm. That one algorithm truly blew my mind the first time Wander implemented it back at WeTransfer. Normally when people start out with rate limiting, a naive implementation would look like this: WebRequest.create!(at_time: Time.now) if WebRequest.where(key: request_ip).where("at_time BETWEEN ? AND ?", 1.minute.ago, Time.now).count &gt; rate_limit raise "Throttled" else WebRequest.create!(key: request_ip, at_time: Time.now) end This has a number of downsides. For one, it creates an incredible number of database rows - and if you are under a DOS attack it can easily tank your database’ performance this way. Second - there is a time gap between the COUNT query and the INSERT of the current request. If your attacker is aggressive enough, they can overflow your capacity between these two calls. And finally - after your throttling window lapses - you need to delete all those rows, since you sure don’t want to keep storing them forever! The next step in this type of implementation is usually “rollups”, with their size depending on the throttling window. The idea of the rollups is to store a counter for all requests that occurred within a specific time interval, and then to calculate over the intervals included in the window. For example, with requests during 5 seconds, we could have a counter with a secondly granularity: if WebRequest.where(key: request_ip).where("second_window BETWEEN ? AND ?", 5.seconds.ago.to_i, Time.now.to_i).sum(:reqs) &gt; rate_limit raise "Throttled" else WebRequest.find_or_create_by(key: request_ip, second_window: Time.now.to_i).increment(:reqs) end This also has its downsides. For one, you need to have good granularity counters, meaning that the interval which you count (be it 1 second, 1 day or 1 hour) need to be cohesive with your throttling limit. You can of course choose to use intervals sized to your throttling window and sum up “current and previous”, but even in that case you will need 2 of those: |••••••• 5s ••T-----|----- 5s -----T•••| There is also an issue with precision. If you look at the above figure, “window counters” would indicate that your rate limit has been consumed even though looking back 5 seconds from now you are not covering the entire 5 second window looking back, but just a portion of it. So if you sum the counter for the current interval and the last, the sum might be higher than the actual resource use for the last 5 seconds. As a matter of fact, this is what early version of prorate did.]]></summary></entry><entry><title type="html">Two other possible reasons juniors are having it tough on the job market</title><link href="https://blog.julik.nl/2022/01/two-other-reasons-juniors-are-having-it-tough" rel="alternate" type="text/html" title="Two other possible reasons juniors are having it tough on the job market" /><published>2022-01-01T00:00:00+00:00</published><updated>2022-01-01T00:00:00+00:00</updated><id>https://blog.julik.nl/2022/01/two-other-reasons-juniors-are-having-it-tough</id><content type="html" xml:base="https://blog.julik.nl/2022/01/two-other-reasons-juniors-are-having-it-tough"><![CDATA[<p>It has recently <a href="https://twitter.com/AmyChanta/status/1476954819885617155">become a hot topic that junior developers are having difficulties finding a job,</a> even though the market is very hot at the moment. Market for senior talent is, yes, but for juniors - not so much. As a matter of fact it has been flagged that it is harder than ever for a beginner to start a career in software. Multiple causes have been outlined:</p>

<ul>
  <li>Companies do not want to invest into talent which is going to demand extreme raises or leave as soon as they make up experience</li>
  <li>Companies assume they are not able to train and teach in a full-remote setting</li>
  <li>Companies assume they need a full-blown training program</li>
</ul>

<p>I would like to raise two extra topics though, which I haven’t seen mentioned. I might be a minority voice here, but I feel they are relevant and we are not giving them due attention.</p>

<p>We really need to change this.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<h3 id="hands-on-work-with-authority-is-considered-malpractice">Hands-on work with authority is considered malpractice</h3>

<p>We made hands-on teaching a career dead-end by idolizing non-coding engineering management.</p>

<p>Software development is, at this stage, a near-blue-collar activity. It is a craft, just like many others - even creative ones - are. Like writing. Like design. Like filmmaking. Craft professions have a fairly standard setup for career growth - you start low, for example in an assistant role, and then “grow through the ranks” as you learn the ins and outs of the profession. This is what mentorship is about. Mentorship, however, has a few interesting prerequisites. You can <em>try</em> to do without them, but you are not likely to succeed.</p>

<ul>
  <li>A mentor does need some amount of authority over the mentee</li>
  <li>A mentor must be doing hands-on work, likely the same project, as the mentee</li>
  <li>There is some amount of accountability on both sides - the mentor is accountable for teaching, and the mentee is accountable for executing well and applying energy to learn</li>
</ul>

<p>The modern setup, however, is that the only way to obtain authority in software engineering teams is to go into <a href="https://www.amazon.com/Elegant-Puzzle-Systems-Engineering-Management/dp/1732265186">engineering management.</a> We explicitly have decided that the setup we want in our teams is not the creative company or crafts one, but a factory floor one. When you need to teach a junior, you must have <em>both</em> authority and responsibility <em>and</em> be doing hands-on work. It is of no use to a junior when their manager asks them “where do you want to be in 5 years” but does not show how to google for errors efficiently.</p>

<p>And we have a structural issue with our “factory floor” desiderata. For growing juniors well you need to be set up for apprenticeship. Not mentoring, not coaching - apprenticeship - and to do that in a structured way a senior needs to be a <a href="https://lethain.com/tech-lead-managers/">tech lead manager.</a> The article I’m linking to explicitly tells you not to do this to your career and not to become one. This advice is not unique: we are spending countless bytes of tweets to convince each other that if you do both code and leadership you will invariably suck at both.</p>

<p>You will be skipping one-on-ones as a manager and you will be forcing your inferior solutions upon your peers as an engineer. You will be doing technical interviews while you ought to be doing culture-fit interviews. You will be figuring how this god-awful framework found its way into your new product instead of <a href="https://www.goodreads.com/book/show/34416720-a-seat-at-the-table">getting a seat at the table.</a> The people who found great success in leadership bash you for <a href="https://www.amywriteswords.com/p/the-sorting-hat">requiring your engineering leadership to do hands on work</a>. Asking engineering managers to code in interviews or to show code is considered bad manners for employers. <em>Phew doing code? Do you need to hire me for that? Am I supposed to be able to do work of my entire team? I thought I am supposed to lead them instead…</em></p>

<p>The end result is that being good at hands-on work <em>and</em> teaching with authority becomes a hot potato. Nobody wants to do it, it is not glamorous, it is not appreciated and it can be…spicy. Every situation where someone who is not the engineering manager has authority is considered “mistrust” and “opportunity for abuse”.</p>

<ul>
  <li>Questioning work assigned by the product manager on your team? Mistrust.</li>
  <li>Pull request reviews with “Request changes”? Mistrust <em>and</em> abuse.</li>
  <li><a href="http://live.julik.nl/2021/11/art-science-taste-and-clean-code">Bikeshedding things?</a> Mistrust <em>and</em> abuse.</li>
</ul>

<p>We ended up in a curious situation where the only way to step off the tech treadmill (changing stacks, frameworks, team reshuffles…) is to go into pure people management. And it works! It does push folks who are successful in management further up in management. The problem with that is, however, that purely people managing a junior likely won’t be enough.</p>

<p>Then we come to the other side of the coin: imagine that you are a successful mid-career developer and you already did lose your shot at people management (either it’s not your thing, or you were labeled “difficult” and <a href="https://staffeng.com/guides/staying-aligned-with-authority/">not aligned with authority</a> or you simply had other commitments like having a family at the moment where career aggression was called for). Doesn’t matter - the fact is that you are this person who people are likely to come to for advice, and receive a kind, helpful response. People come to you for solving problems. You have opinions, and you are prepared to share them if listened to. Apparently, some of those opinions are valuable – as people keep listening.</p>

<p>Now you get an opportunity to teach a junior. But beware! You may not decide <em>what</em> the scope of the work is going to be - for that is the purview of the product manager. You may not decide <em>what work</em> to assign - that is the purview of the scrum master or of your engineering manager. You may not dictate solutions either - that is no longer the purview of anybody, because we don’t do authority here, only influencing. Here is what you might potentially get instead:</p>

<ul>
  <li>HR might be on your tail because you might say something that will be considered unacceptable, and since you are not allowed to say it (you are not in management): congratulations, go get your whipping.</li>
  <li>If the junior person proves to be super-talented and then leaves for a FAANG you are considered not careful because you haven’t “retained” the person. Later you find out they left purely because of pay - you did not know what the pay was, neither did you know it was a topic. A few well-placed pay bumps along the way could have solved that nicely at a 10K expense, but you are not supposed ot know that. But you contributed to “churn”.</li>
  <li>If the junior person proves not to make progress (as judged by all the now-as-prescribed-not-coding managers on your team) it is again your fault, because you were the assigned person to help the junior grow. You could have signaled 3 quarters back that the person was not pulling the load, but that is not your job - it is the job of the engineering manager. Yet again: you are responsible.</li>
  <li>You see that the junior person is not making progress because they were placed on a dead-end, “study” project which is not even going to go to production, ever. You know the junior will be judged for underperforming on shit work nobody needs doing. Junior underperforms. You are responsible.</li>
</ul>

<p>Now, these are the upfront “bad” scenarios. Imagine none of this happens, and you successfully help a human grow through the ranks and become better. You do get a chance of having this noted on your reviews, and it is wonderful. That’s it. No - really - <em>that’s it.</em> All the other things, seen from the corporate career perspective, are tarpits of doom - and more than a couple of them carry the extra danger of labeling you “difficult”. If you already have that label, you are getting the second - and you know what a third could mean.</p>

<p>What do we do if we want to survive in a workforce - especially in one where situation can be very precarious due to the pandemic, remote work, shifting economic landscapes etc.? Where you, as a mid-career developer, likely already have a family and children - and <a href="https://www.cnbc.com/2021/12/18/netherlands-is-going-into-lockdown-again-starting-sunday-to-curb-omicron-infections.html">hell the daycares close again ffs</a>? Will you take <em>all those extra risks</em> for all that reward of seeing another person shine? Just <em>one other person?</em></p>

<p>Maybe you will. If you are just crazy enough. But most people won’t. <em>Teaching craft</em> has become not only unfashionable but can also be dangerous (a lot of grief and frustration in people management that should be directed way higher up into the chain of command ends up shot as flak at the people who can be accused more easily - peers).</p>

<p>Once it becomes unfashionable <em>and</em> risky for their careers, are we really that surprised that mediors and seniors prefer to stay away from mentorship? Are we really? Just look at <a href="https://bridgeteams.com/blog/tech-lead-vs-engineering-manager-differences-salaries-hiring-tips/">these stats</a> - “engineering manager” openings outnumber the “tech lead” openings 4 to 1.</p>

<p>Let me recap: we have made “hands-on leadership” frowned upon. “Hands-on leadership” is a requirement for teaching. We might have made a mistake.</p>

<h3 id="too-many-reorgs">Too many reorgs</h3>

<p>Mentor/mentee relationships take months to establish. Our reorg fetish (“change is the only constant”) and the like destroys them.</p>

<p>A relationship not only takes long to form – it implies that a great deal of trust has to exist on both ends - the mentee must trust that when their mentor makes certain choices, these choices are to their common benefit. The mentor must trust that the mentee does not want to filibuster them and is not using them as a stepping stone to something they did not manage to obtain - like a position in engineering management with a full bypass of hands-on work, that the mentee is not going to shit on them in front of customers or stakeholders, and the like. This trust does not magically appear by virtue of having a meeting with your mutual engineering manager and shaking hands. It gets forged over months of serving together, under the same flag and on the same team. One of the most valuable things that comes out from craft relationships like this are the intangibles, the ungooglables. Who is the crazy exec in the room? Who is likely to give deadlines which are compressed by a factor of 3, and for what reason? How do we approach unreasonable requirements? How do we debate solutions?</p>

<p>The reality is that modern software teams often reshuffle once a quarter. The cadence I have last been in was once every 6 months for the entire organisation, and at least just as often for the team. Every reorg would bring hire-above into the picture, destroying any reasonable relationships of influence-without-authority that could have been there to begin with. Some teams existed for a few weeks, getting disbanded right after their project got completed. No stable pairs of mentor-mentee could form, the only constant would be the managers who would go ever higher in reach and headcount.</p>

<p>A lot of those reorgs do not hold the interests of the mentor or the mentee in their sights - they are often done instead to accomodate newly hired upper management layers, or to achieve other political goals which do not have anything to do with output.</p>

<p>In this situation, for a junior, placing their trust in a mentor is also incredibly risky! What point is there in confiding in someone if by next month you are in completely different departments, working on completely divergent projects? Is it really conductive to one’s career examining the work style of all those 6 different seniors you are going to work with throughout the year, especially if half of them quit by year’s end?</p>

<p>If we wanted to put an end to this: for the first year of a junior’s journey, if they bond with their mentor, only reorganize them in pairs. Will the modern school of software reorgs ascend to that principle? Unlikely.</p>

<p>Let me recap: with our fetish of “I must change so I can stay the same” we have relegated structured hands-on teaching to organisations who can run their own vocational schools. Our insatiable lust for promotions (and thus reorgs!) makes it impossible for proper bonds between mentors and mentees to form. We might have made a mistake.</p>

<h3 id="all-of-this-is-pretty-sad">All of this is pretty sad</h3>

<p>In my <a href="https://www.imdb.com/name/nm3127224/">previous line of work</a> most of my mentors were working on the same projects I was working on. They would hand down tasks, split tasks, discuss work - but also they would shoulder in time of need. They would have the guts to say “we have to do this thing together, it is absolutely bollocks but this is what the client wants and we cannot play around it. The shortest path to spend the least possible time on the bollocks thing is to…” With the best ones I’ve had I knew, always, that they would be able to do the task I had to accomplish if I were to fail - and this was clearly communicated. At all times. It is an incredibly empowering feeling.</p>

<p>Afterwards came the time when a most gratifying thing in the world is seeing how someone you teach becomes better than you were. Seeing people achieve something. Seeing them “get it”.</p>

<p>By arranging the setup against newcomers in the profession we rob folks of the success of growing into capable professionals. We are settling nicely into our big tree, and pulling the ladder up with us.</p>

<p>I doubt we, as a community of practitioners, are going to benefit from this in the long run.</p>

<h3 id="how-this-manifests">How this manifests</h3>

<p>You need an extra person for your team, and when you go “maybe we should consider less senior talent and maybe we could provide them some support?..” and your most senior folks all go like “it might be difficult with the current workload”, “we have so much going on at the moment”, “it might be difficult with teams being so volatile”. Ask yourself: <em>are they saying what they are really saying?</em> Or are they aware that the organisation has clearly indicated that making other humans progress is not getting them promoted? What are the dangers they are trying to avoid by saying “no”? What are they afraid of? And how is your leadership responsible for this situation? How can you help this situation change? Will you give them actual responsibilities – but also opportunities if they say “yes”? Will you provide them extra comp? Extra training (on how to mentor: yes that is a thing)? Will we give them some latitude in defining the project scope to work together with their mentee?</p>

<h3 id="what-is-there-to-do">What is there to do?</h3>

<p>Well, some things. Some of these things are right against the mainstream line of thinking these days, some are just my personal positions. I have debated many of them with many folks, and lots of good (and bad) conversations were had. If I could summarise to just a handful:</p>

<ul>
  <li>Consider the balance between hands-on people vs. purely people managers vs. execs vs. absolutely unrelated people who have nothing to do there amongst the “deciders” on your open position. Any open position with hands-on work touching material (design and engineering). I know you have deciders, don’t shy away. Look at the balance and now consider again - are they the best people to be where they are? Do they <em>have</em> to decide on that opening? Will the organisation lose with every false negative, every person who had potential to become great but was dropped?</li>
  <li>Have a solid internship program (you don’t have to be a huge shop to try, here in NL there is a very solid framework for it). Even smaller companies can do it.</li>
  <li>We have overcompensated against biases and while at it we have forbidden hands-on people to have authority, thinking that “purely people management” folks will be better at it and less biased, no matter what. Good for people management folks, might be not so good for everybody else. You win some but you also lose some. Loosen the collar.</li>
  <li>Stop deriding people who both code and manage. It is ok, and a plethora of creative industries before us were, are, and will be doing this. We (with our kubernetes clusters and React hooks) are not special.</li>
</ul>

<p>The current mainstream approach to managing engineering teams steals authority from engineers, and thus destroys apprenticeship – which is required to bring new folks into the profession. We should reconsider.</p>

<p>I hope this was interesting, and – despite all of the above – let’s hope a great number of amazing, talented junior people will join us in the joy of crafting beautiful things together in 2022. 🥂</p>]]></content><author><name></name></author><summary type="html"><![CDATA[It has recently become a hot topic that junior developers are having difficulties finding a job, even though the market is very hot at the moment. Market for senior talent is, yes, but for juniors - not so much. As a matter of fact it has been flagged that it is harder than ever for a beginner to start a career in software. Multiple causes have been outlined: Companies do not want to invest into talent which is going to demand extreme raises or leave as soon as they make up experience Companies assume they are not able to train and teach in a full-remote setting Companies assume they need a full-blown training program I would like to raise two extra topics though, which I haven’t seen mentioned. I might be a minority voice here, but I feel they are relevant and we are not giving them due attention. We really need to change this.]]></summary></entry><entry><title type="html">Actually creating a gem for idempotency keys</title><link href="https://blog.julik.nl/2021/12/actually-creating-an-idempotency-keys-gem" rel="alternate" type="text/html" title="Actually creating a gem for idempotency keys" /><published>2021-12-25T00:00:00+00:00</published><updated>2021-12-25T00:00:00+00:00</updated><id>https://blog.julik.nl/2021/12/actually-creating-an-idempotency-keys-gem</id><content type="html" xml:base="https://blog.julik.nl/2021/12/actually-creating-an-idempotency-keys-gem"><![CDATA[<p>I’ve already touched on it a bit in the <a href="/2021/11/do-the-scariest-thing-first">article about doing the scariest thing first</a> – one of the things we managed to do at WeTransfer before I left was implementing proper idempotency keys for our storage management system (called Storm). The resulting gem is called <a href="https://github.com/julik/idempo">idempo</a> and you use it about like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">config</span><span class="p">.</span><span class="nf">middleware</span><span class="p">.</span><span class="nf">insert_after</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Head</span><span class="p">,</span> <span class="no">Idempo</span><span class="p">,</span> <span class="ss">backend: </span><span class="no">Idempo</span><span class="o">::</span><span class="no">RedisBackend</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="no">Redis</span><span class="p">.</span><span class="nf">new</span><span class="p">)</span>
</code></pre></div></div>

<p>It is great and you should try it out. If you are pressed for time, <strong>TL;DR:</strong> we built a gem for idempotency keys in Rack applications. It was way harder than we expected, and we could not find an existing one. As a community we do not publish enough details about how software gets designed, which makes it less likely that our software will be found and used. I don’t want this to happen to idempo. Making a gem which does a seemingly tiny thing can be devilishly complex, and switchable implementations for things are actually useful.</p>

<p><strong>Disclaimer:</strong> consider all code here to be pseudocode. For actual working versions of the same check out the code in idempo itself.</p>

<h3 id="why-did-we-even-need-it">Why did we even need it?</h3>

<p>To recap: idempotency keys allow you to reject double requests to modify the same resource (or to apply the same modification to the same resource), and they map pretty nicely both to REST HTTP endpoints and to RPC endpoints. Normally idempotency keys are implemented using a header. For a good exposition on idempotency keys, check out the two articles by Brandur Leach here - the <a href="https://brandur.org/idempotency-keys">first one</a> gives a nice introduction, and the <a href="https://www.brandur.org/fragments/idempotency-key-draft">second one</a> gives a much more actionable set of guidelines for implementing one.</p>

<p>The point where we realised that we will need idempotency keys in the first place came about when we decided to let other teams use a JavaScript module that we would provide. The module - and the upload protocol WeTransfer uses - is peculiar in that it has quite a bit of implicit state. Multiple requests are necessary, and they need to be synchronised somewhat carefully. Requests should be retried, because we were already using a lot of autoscaling - so a server could end up dying during a request. Yet some of the operations we let our JS client perform (such as creating a new transfer) must be atomic - you can only create a transfer once, and there is some bookkeeping involved when doing that. The transfer is going to have a pre-assigned ID, and if the client attempts to create a transfer and then does not register properly that the transfer got created the ID will end up taken. This bookkeeping touches the database, and thus creates database load. Also, the output of those operations can be cached for some time. In the past, we had situations where an uploader would end up in an endless loop (due to problems with retry logic for example) and would hit the same endpoint, near-endlessly, and very frequently. If we had an idempotency key system we could significantly reduce the impact this had on our systems – and avoid a number of production incidents. So with the new JS client we wanted to make it support an idempotency key for the entire upload process for your transfer, and we wanted to have this idempotency key be transparently used on the server.</p>

<p>As a matter of fact, also our iOS app ended up implementing idempotency keys in the same way - and with the same benefits.</p>

<p>Surprisingly to us, while Ilja Eftimov has made a good write up about idempotency keys and made a demo of an implementation in <a href="https://ieftimov.com/post/understand-how-why-add-idempotent-requests-api/">this article</a> we were surprised to find no proper gems for idempotency keys existed, which we could pick off the shelf. So some brainstorming and a little pondering later we decided that we had to make one, albeit only for our storage manager system. It is not that Ilja’s code is bad – it just omits a few interesting side-effects which might be more frequent than we could think of initially.</p>

<p>This article is long, and there are a few things I want to touch on here.</p>

<p>Before we move any further: idempo came about with great help from <a href="https://twitter.com/lorenzograndi4">Lorenzo Grandi</a> and <a href="https://twitter.com/pablocrivella">Pablo Crivella</a>, sending my hugs to both. Lorenzo is also in the fabulous new Honeypot documentary that <a href="https://www.youtube.com/watch?v=1-Vzby0PkBw">you can find here.</a></p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<hr />

<ul>
  <li>It takes quite a bit of work not only to build, but to <em>design</em> a library. I want to cover the <em>design</em> part here in more detail, because this is what is often too touchy-feely to discuss overtly (so so subjective, oh my) and yet what often is not done very well</li>
  <li>It indeed does take quite a bit of work to build something like this <em>and account for the variety of semantics to be compatible with.</em> In this particular instance, not adhering to the semantics of your datastore will make your library do things you do not want it to do, and since the library here is about providing guarantees: contrary to <a href="/2021/12/the-value-of-not-having-to-be-right">this article</a> here you <strong>have to be correct</strong> or your software doesn’t work.</li>
  <li>There are a lot of good libraries out there. Making a good library takes work. I believe <a href="https://github.com/julik/idempo">idempo</a> turned out a very, very good library. It does deserve some more spotlight, and I suck at marketing.</li>
</ul>

<p>So, let’s look at the idempotency key decision tree:</p>

<ul>
  <li>Receive a request. If the request contains the idempotency key header:
    <ul>
      <li>Check whether this idempotency key has already been recorded, and whether there is a rendered response for it.</li>
      <li>If the key exists and rendered response is cached: return the cached response</li>
      <li>If the key exists: consider the request “in progress” and consider this request to be a concurrent request. Respond with a 409 status.</li>
      <li>If the key doesn’t exist: save it someplace (so that no concurrent request can begin) and enter the application code</li>
      <li>Once the application is done, capture the rendered response.
        <ul>
          <li>If the response is idempotent (4xx error or 2xx or 3xx) cache the response</li>
          <li>If the response is not idempotent - release the idempotency key so that a subsequent request may succeed instead</li>
        </ul>
      </li>
      <li>Return the response</li>
    </ul>
  </li>
</ul>

<p>This looks pretty simple, and mostly it is - this is what Ilja’s code is doing also, for the most part. Let’s imagine a hypothetical Rack middleware which does the above:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">IdempotencyKeyMiddleware</span> <span class="o">&lt;</span> <span class="no">Struct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:app</span><span class="p">)</span>
  <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
     <span class="k">return</span> <span class="n">app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="k">unless</span> <span class="n">idempo_key</span> <span class="o">=</span> <span class="n">env</span><span class="p">[</span><span class="s1">'HTTP_X_IDEMPOTENCY_KEY'</span><span class="p">]</span>
     <span class="k">if</span> <span class="n">request_in_progress?</span><span class="p">(</span><span class="n">idempo_key</span><span class="p">)</span>
       <span class="k">return</span> <span class="p">[</span><span class="mi">409</span><span class="p">,</span> <span class="p">{},</span> <span class="p">[</span><span class="s2">"Concurrent request in progress, try later!"</span><span class="p">]]</span> <span class="k">if</span> 
     <span class="k">elsif</span> <span class="n">recorded_response_exists?</span><span class="p">(</span><span class="n">idempo_key</span><span class="p">)</span>
       <span class="k">return</span> <span class="n">fetch_response_triplet</span><span class="p">(</span><span class="n">idempo_key</span><span class="p">)</span>
     <span class="k">else</span>
      <span class="n">record_request_started!</span><span class="p">(</span><span class="n">idempo_key</span><span class="p">)</span>
      <span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="n">app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
      <span class="k">if</span> <span class="n">status</span> <span class="o">&lt;</span> <span class="mi">500</span> <span class="o">&amp;&amp;</span> <span class="n">status</span> <span class="o">&gt;=</span> <span class="mi">200</span>
        <span class="n">save_response</span><span class="p">(</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span>
      <span class="k">end</span>
      <span class="n">delete_in_progress_response</span><span class="p">(</span><span class="n">idempo_key</span><span class="p">)</span>
      <span class="p">[</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">]</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The implementations of <code class="language-plaintext highlighter-rouge">request_in_progress?</code>, <code class="language-plaintext highlighter-rouge">recorded_response_exists?</code> and the like are omitted here, because these are not the “tricky bits”. We will need to account for a few things: <code class="language-plaintext highlighter-rouge">429</code> is actually a code that has to be declared non-idempotent, because the client is supposed to retry that one. <code class="language-plaintext highlighter-rouge">409</code> is also not very suitable if you are already using it for something else (like validation errors) – also for us we had automatic use of the <code class="language-plaintext highlighter-rouge">Retry-After</code> in our JS client, so a 429 made more sense for us for a concurrent request.</p>

<p>We also need to add the request fingerprint (for example the URL of the request, and likely also the digest of the request body) to the idempotency key (and call it something else - like <code class="language-plaintext highlighter-rouge">request_key</code>).</p>

<p>But the bigger problem with this code though - and with most of Ilja’s examples for that matter (in the article I’ve linked above) is that they do not account well for race conditions. Specifically:</p>

<ul>
  <li>Between <code class="language-plaintext highlighter-rouge">recorded_response_exists?</code> and <code class="language-plaintext highlighter-rouge">fetch_response_triplet</code> the record could disappear from the datastore 🏎.</li>
  <li>During <code class="language-plaintext highlighter-rouge">fetch_response_triplet</code> the record could disappear from the datastore (if the read from the datastore is not atomic) 🏎.</li>
  <li>Between <code class="language-plaintext highlighter-rouge">record_request_started!</code> (just before it gets called) another request could enter and try to do the same, and get to the <code class="language-plaintext highlighter-rouge">record_request_started!</code> line earlier than ours. If that happens, both our request <em>and</em> the other request will be allowed to do <code class="language-plaintext highlighter-rouge">app.call()</code> and the idempotency guarantee is going to be violated 🏎</li>
  <li>Between <code class="language-plaintext highlighter-rouge">record_request_started!</code> and <code class="language-plaintext highlighter-rouge">save_response</code> the record could also disappear. Depending on the datastore semantics we might be unable to save it. 🏎.</li>
  <li>If our <code class="language-plaintext highlighter-rouge">app.call</code> crashes we might not perform <code class="language-plaintext highlighter-rouge">delete_in_progress_response</code>, same if we crash before we perform our idempotent action. 🏎.</li>
</ul>

<p>This kind of problem is fairly well known and it calls for the use of a shared lock. Given a request key of <code class="language-plaintext highlighter-rouge">RK</code> only one request should be able to check it out, and only one request should hold it while it is processing the request or looking for a cached response. When locks are used, we need to carefully adjust the code so that all the spots where we operate with the lock held - but also all the spots where we operate without it - can be examined and analysed for races. So, instead of using <code class="language-plaintext highlighter-rouge">find</code> (like Ilja is suggesting) we need to use <code class="language-plaintext highlighter-rouge">create_or_find</code>, but since we start on a lower level - without using ActiveRecord or Redis - we can do it in a bit more minimal fashion:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
  <span class="k">return</span> <span class="n">app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="k">unless</span> <span class="n">env</span><span class="p">[</span><span class="s1">'HTTP_X_IDEMPOTENCY_KEY'</span><span class="p">]</span>
  <span class="n">request_key</span> <span class="o">=</span> <span class="n">compute_request_key</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
  <span class="n">did_acquire_lock</span> <span class="o">=</span> <span class="n">acquire_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
  <span class="k">return</span> <span class="p">[</span><span class="mi">429</span><span class="p">,</span> <span class="p">{},</span> <span class="p">[</span><span class="s1">'Concurrent request!'</span><span class="p">]]</span> <span class="k">unless</span> <span class="n">did_acquire_lock</span>
  <span class="n">response_triplet</span> <span class="o">=</span> <span class="n">fetch_response_triplet</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
  <span class="k">return</span> <span class="n">response_triplet</span> <span class="k">if</span> <span class="n">response_triplet</span>
  <span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="n">app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
  <span class="n">save_response</span><span class="p">(</span><span class="n">request_key</span><span class="p">,</span> <span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span> <span class="k">if</span> <span class="n">response_idempotent?</span><span class="p">(</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span>
  <span class="p">[</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">]</span>
<span class="k">ensure</span>
  <span class="n">release_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span> <span class="k">if</span> <span class="n">did_acquire_lock</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Note that we have collapsed the <code class="language-plaintext highlighter-rouge">recorded_response_exists?</code> and <code class="language-plaintext highlighter-rouge">fetch_response_triplet</code> into one call to avoid the race condition where our response could disappear between our check and our fetch. This implies that the <code class="language-plaintext highlighter-rouge">fetch_response_triplet</code> will also do something to use the most atomic datastore function we can apply.</p>

<p>But more importantly: we also introduced a lock, which we can either receive correctly (<code class="language-plaintext highlighter-rouge">did_acquire_lock</code> will be <code class="language-plaintext highlighter-rouge">true</code>) or get denied - in which case we know that a concurrent request is already in progress. Notice a pattern here: “tell, don’t ask” is how you can reduce race conditions fairly quickly. This is why instead of <code class="language-plaintext highlighter-rouge">find</code> and then <code class="language-plaintext highlighter-rouge">create</code>, first collapse to <code class="language-plaintext highlighter-rouge">find_or_create</code> - and immediately after to <code class="language-plaintext highlighter-rouge">create_or_find</code> which first tells the database to create the record, and then atomically finds it, although that <em>too</em> has a race condition – we will get there soon enough.</p>

<p>Let’s look at our locking in a bit more detail. If we remove other code, we see the following code paths touching it:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">did_acquire_lock</span> <span class="o">=</span> <span class="n">acquire_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
<span class="n">release_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span> <span class="k">if</span> <span class="n">did_acquire_lock</span>
</code></pre></div></div>

<h3 id="doing-the-simplest-thing-first">Doing the simplest thing first</h3>

<p>To make these do something useful, we are going to need us a lock issuing mechanism. Since we “start small”, we will start with an in-process locking service. The simplest thing for holding a number of locks (which are just strings) would be a <code class="language-plaintext highlighter-rouge">Set</code>. If the string for a request key is in our set, we know a request is in progress. If the string is not in the set, we add it to our set and return <code class="language-plaintext highlighter-rouge">true</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">IdempotencyKeyMiddleware</span>
   <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>
     <span class="vi">@app</span> <span class="o">=</span> <span class="n">app</span>
     <span class="vi">@requests_in_progress</span> <span class="o">=</span> <span class="no">Set</span><span class="p">.</span><span class="nf">new</span>
   <span class="k">end</span>
   
   <span class="k">def</span> <span class="nf">acquire_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
     <span class="k">return</span> <span class="kp">false</span> <span class="k">if</span> <span class="vi">@requests_in_progress</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
     <span class="vi">@requests_in_progress</span> <span class="o">&lt;&lt;</span> <span class="n">request_key</span>
     <span class="kp">true</span>
   <span class="k">end</span>
   
   <span class="k">def</span> <span class="nf">release_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
     <span class="vi">@requests_in_progress</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
   <span class="k">end</span>
   
   <span class="o">...</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Notice how we introduced yet another race condition here: our middleware is multi-threaded. Two requests can (and will!) call into <code class="language-plaintext highlighter-rouge">acquire_lock</code>) around the same time, and both of them may get “through” to <code class="language-plaintext highlighter-rouge">@requests_in_progress &lt;&lt; request_key</code> operation. Only one of them will add the request key to the Set, but the end result will be the same - we will allow 2 concurrent requests. A Rack middleware gets instantiated once – the <a href="https://www.rubydoc.info/github/rack/rack/master/Rack/Builder">Rack builder</a> creates you one copy of your application tree, and then dispatches multiple calls into it. Our locking service thus can be multithreaded.</p>

<p>This is why we need a Mutex here - we may only allow one thread to do things to our locks Set at the time:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">IdempotencyKeyMiddleware</span>
   <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>
     <span class="vi">@app</span> <span class="o">=</span> <span class="n">app</span>
     <span class="vi">@requests_in_progress</span> <span class="o">=</span> <span class="no">Set</span><span class="p">.</span><span class="nf">new</span>
     <span class="vi">@requests_in_progress_mutex</span> <span class="o">=</span> <span class="no">Mutex</span><span class="p">.</span><span class="nf">new</span>
   <span class="k">end</span>
   
   <span class="k">def</span> <span class="nf">acquire_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
     <span class="vi">@requests_in_progress_mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
       <span class="k">return</span> <span class="kp">false</span> <span class="k">if</span> <span class="vi">@requests_in_progress</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
       <span class="vi">@requests_in_progress</span> <span class="o">&lt;&lt;</span> <span class="n">request_key</span>
       <span class="kp">true</span>
     <span class="k">end</span>
   <span class="k">end</span>

   <span class="k">def</span> <span class="nf">release_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
     <span class="vi">@requests_in_progress_mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
       <span class="vi">@requests_in_progress</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
     <span class="k">end</span>
   <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>We also need to do the same when we remove our request key from the locks Set too, so that our <code class="language-plaintext highlighter-rouge">Set#delete</code> does not happen right between the <code class="language-plaintext highlighter-rouge">include?</code> and the <code class="language-plaintext highlighter-rouge">&lt;&lt;</code> being called by another thread. Note that we will only lock the mutex when we are changing or reading the contents of the <code class="language-plaintext highlighter-rouge">Set</code> but not for the entire duration of our request - that would make our app single-threaded, and we don’t want that.</p>

<p>Next step: make some machinery to cache a generated response. Careful there too: Rack bodies are not rewindable <em>and</em> they cannot be serialized to JSON because they can be binary, and the like (but this is fairly trivial to do):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">rack_body_to_array_and_cached_version</span><span class="p">(</span><span class="n">rack_body</span><span class="p">)</span>
  <span class="p">[].</span><span class="nf">tap</span> <span class="k">do</span> <span class="o">|</span><span class="n">chunks</span><span class="o">|</span>
    <span class="n">rack_body</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">chunk</span><span class="o">|</span> <span class="n">chunks</span> <span class="o">&lt;&lt;</span> <span class="n">chunk</span> <span class="p">}</span>
  <span class="k">end</span>
<span class="k">ensure</span>
  <span class="n">rack_body</span><span class="p">.</span><span class="nf">close</span> <span class="k">if</span> <span class="n">rack_body</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Let’s recap our race condition situation now:</p>

<ul>
  <li>Between <code class="language-plaintext highlighter-rouge">recorded_response_exists?</code> and <code class="language-plaintext highlighter-rouge">fetch_response_triplet</code> the record could disappear from the datastore - <strong>Fixed</strong> as we are inside the locked region there</li>
  <li>During <code class="language-plaintext highlighter-rouge">fetch_response_triplet</code> the record could disappear from the datastore (if the read from the datastore is not atomic) <strong>Still there</strong></li>
  <li>Between <code class="language-plaintext highlighter-rouge">record_request_started!</code> (just before it gets called) another request could enter and try to do the same, and get to the <code class="language-plaintext highlighter-rouge">record_request_started!</code> line earlier than ours. If that happens, both our request <em>and</em> the other request will be allowed to do <code class="language-plaintext highlighter-rouge">app.call()</code> and the idempotency guarantee is going to be violated - <strong>Fixed</strong> as we are inside the locked region</li>
  <li>Between <code class="language-plaintext highlighter-rouge">record_request_started!</code> and <code class="language-plaintext highlighter-rouge">save_response</code> the record could also disappear. Depending on the datastore semantics we might be unable to save it. <strong>Fixed</strong> as we are inside the locked region</li>
  <li>If our <code class="language-plaintext highlighter-rouge">app.call</code> crashes we might not perform <code class="language-plaintext highlighter-rouge">delete_in_progress_response</code>, same if we crash before we perform our idempotent action. <strong>Fixed</strong> - with in-memory locks, if our process crashes our locks table goes with it. If our app raises we will still release the lock via an <code class="language-plaintext highlighter-rouge">ensure</code> block.</li>
</ul>

<p>We solved a number of the races, but we still have a couple.</p>

<ul>
  <li>We need to coordinate the saving and deletion of the cached response with our locks somehow. Response disappearing when a lock is held violates consistency</li>
  <li>We need to ensure that our locks are properly released - for now this is not an issue. It will be in a second.</li>
</ul>

<p>Now, this is all fine and well as long as we are fine running just one Puma server with a few threads. Most applications will not work like this - they will have multiple servers, all running multiple application threads. So a shared datastore of some kind is going to be needed. For us, the store of choice “by default” was the database as we have not yet introduced Redis or Memcached into the app. We found, at the time, that even though these datastores are amazing - we could get some benefits by only using one datastore if we could get by with that datastore exclusively. But at the same time - with the history of libraries like <a href="https://github.com/WeTransfer/prorate">prorate</a> - we would be getting Redis onboard, just not right now.
So…</p>

<h3 id="bring-us-the-pluggable-data-stores">Bring us the pluggable data stores!</h3>

<p>This, in turn, brings us to an interesting junction. When we want to work with locks, and especially distributed locks - and if we want to coordinate persistence for responses with those locks - the semantics of the datastore we pick <em>matter.</em> They <em>matter</em> a hell of a lot actually, because some of them do support “check then set”, others only support “set and recover”. Some support foreign keys which get validated, some don’t. Some are able to have native TTL support - which we needed - and some didn’t. So the difficulty turned out to be to create an API which would allow us to use different datastores for idempotency keys, but crucially with <em>the same semantics</em> regardless of how the implementation specific to the datastore works. So inside of our code, we would likely want to do something like this (because this is what the objects are for):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
  <span class="k">return</span> <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="k">unless</span> <span class="n">env</span><span class="p">[</span><span class="s1">'HTTP_X_IDEMPOTENCY_KEY'</span><span class="p">]</span>
  <span class="n">request_key</span> <span class="o">=</span> <span class="n">compute_request_key</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>

  <span class="n">did_acquire_lock</span> <span class="o">=</span> <span class="vi">@datastore</span><span class="p">.</span><span class="nf">acquire_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
  <span class="k">return</span> <span class="p">[</span><span class="mi">429</span><span class="p">,</span> <span class="p">{},</span> <span class="p">[</span><span class="s1">'Concurrent request!'</span><span class="p">]]</span> <span class="k">unless</span> <span class="n">did_acquire_lock</span>

  <span class="n">response_triplet</span> <span class="o">=</span> <span class="vi">@datastore</span><span class="p">.</span><span class="nf">fetch_response_triplet</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
  <span class="k">return</span> <span class="n">response_triplet</span> <span class="k">if</span> <span class="n">response_triplet</span>

  <span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
  <span class="vi">@datastore</span><span class="p">.</span><span class="nf">save_response</span><span class="p">(</span><span class="n">request_key</span><span class="p">,</span> <span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span> <span class="k">if</span> <span class="n">response_idempotent?</span><span class="p">(</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span>
  <span class="p">[</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">]</span>
<span class="k">ensure</span>
  <span class="vi">@datastore</span><span class="p">.</span><span class="nf">release_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span> <span class="k">if</span> <span class="n">did_acquire_lock</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now, instead of calling our own methods for locks, we would be calling into this arbitrary <code class="language-plaintext highlighter-rouge">@datastore</code> implementation, which would then use either MySQL, or Redis, or the in-memory Set setup, whichever would be more applicable. Now, how would it look like? For our in-memory setup it would be pretty straightforward:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">LocalIdempotencyStore</span>
   <span class="k">def</span> <span class="nf">initialize</span>
     <span class="vi">@requests_in_progress</span> <span class="o">=</span> <span class="no">Set</span><span class="p">.</span><span class="nf">new</span>
     <span class="vi">@requests_in_progress_mutex</span> <span class="o">=</span> <span class="no">Mutex</span><span class="p">.</span><span class="nf">new</span>
     <span class="vi">@saved_requests</span> <span class="o">=</span> <span class="p">{}</span>
   <span class="k">end</span>

   <span class="k">def</span> <span class="nf">fetch_response_triplet</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
     <span class="k">return</span> <span class="k">unless</span> <span class="vi">@saved_requests</span><span class="p">[</span><span class="n">request_key</span><span class="p">]</span>
     <span class="no">Marshal</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="vi">@saved_requests</span><span class="p">[</span><span class="n">request_key</span><span class="p">])</span>
   <span class="k">end</span>
   
   <span class="k">def</span> <span class="nf">save_response</span><span class="p">(</span><span class="n">request_key</span><span class="p">,</span> <span class="o">*</span><span class="n">triplet</span><span class="p">)</span>
     <span class="vi">@saved_requests</span><span class="p">[</span><span class="n">request_key</span><span class="p">]</span> <span class="o">=</span> <span class="no">Marshal</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">triplet</span><span class="p">)</span>
   <span class="k">end</span>
   
   <span class="k">def</span> <span class="nf">acquire_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
     <span class="vi">@requests_in_progress_mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
       <span class="k">return</span> <span class="kp">false</span> <span class="k">if</span> <span class="vi">@requests_in_progress</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
       <span class="vi">@requests_in_progress</span> <span class="o">&lt;&lt;</span> <span class="n">request_key</span>
       <span class="kp">true</span>
     <span class="k">end</span>
   <span class="k">end</span>

   <span class="k">def</span> <span class="nf">release_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
     <span class="vi">@requests_in_progress_mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
       <span class="vi">@requests_in_progress</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
     <span class="k">end</span>
   <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="bring-us-the-database">Bring us the database!</h3>

<p>We know that <code class="language-plaintext highlighter-rouge">save_response</code> and <code class="language-plaintext highlighter-rouge">fetch_response_triplet</code> are not going to be called without the lock being held, because our lock is indefinite (and stored in memory). Now let’s up this a notch and implement the same based on ActiveRecord - so that our requests go into the database.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ARIdempotencyStore</span>
  <span class="k">def</span> <span class="nf">acquire_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
    <span class="c1"># We presume there is a `requests_in_progress` table with a `request_key` column which has a uniqueness constraint,</span>
    <span class="c1"># that is crucial!</span>
    <span class="no">RequestInProgress</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="kp">true</span>
  <span class="k">rescue</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">RecordNotUnique</span>
    <span class="kp">false</span>
  <span class="k">end</span>
  <span class="k">def</span> <span class="nf">release_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
    <span class="no">RequestInProgress</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">request_key: </span><span class="n">request_key</span><span class="p">).</span><span class="nf">delete_all</span>
  <span class="k">end</span>
  <span class="o">...</span>
<span class="k">end</span>
</code></pre></div></div>

<p>but here already be dragons. Unlike our <code class="language-plaintext highlighter-rouge">LocalIdempotencyStore</code> our implementation here is not immune to sudden process death or termination. If we can <code class="language-plaintext highlighter-rouge">ensure</code> then yes - our held lock will be released. But if our process crashes outright (the server loses the network connection, or shuts down, or the application gets killed abruptly) our locking record will stay in the database indefinitely! And no client will be able to call this endpoint again with this idempotency key, as every request would be considered concurrent. Congratulations: we introduced a race condition due to different datastore semantics. We can circumvent that by using <a href="https://dev.mysql.com/doc/refman/5.7/en/locking-functions.html">advisory locks</a> instead of rows. If our process crashes, it releases the database connection (which is holding the lock) and that, in turn, will make MySQL release the lock itself:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ARIdempotencyStore</span>
  <span class="k">def</span> <span class="nf">acquire_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
    <span class="n">safe_key</span> <span class="o">=</span> <span class="no">Digest</span><span class="o">::</span><span class="no">SHA256</span><span class="p">.</span><span class="nf">hexdigest</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
    <span class="n">result</span> <span class="o">=</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection</span><span class="p">.</span><span class="nf">select_value</span><span class="p">(</span><span class="s2">"SELECT GET_LOCK('%s', 0)"</span> <span class="o">%</span> <span class="n">safe_key</span><span class="p">)</span>
    <span class="n">result</span> <span class="o">==</span> <span class="mi">1</span> <span class="c1"># 1 means we did acquire the lock</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">release_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
    <span class="n">safe_key</span> <span class="o">=</span> <span class="no">Digest</span><span class="o">::</span><span class="no">SHA256</span><span class="p">.</span><span class="nf">hexdigest</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
    <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection</span><span class="p">.</span><span class="nf">select_value</span><span class="p">(</span><span class="s2">"SELECT RELEASE_LOCK('%s')"</span> <span class="o">%</span> <span class="n">safe_key</span><span class="p">)</span>
  <span class="k">end</span>
  <span class="o">...</span>
<span class="k">end</span>
</code></pre></div></div>

<p>You want to do the same with PostgreSQL? Different locking functions, slightly different semantics again, more implementation changes.</p>

<h3 id="bring-us-the-redis-and-more-divergent-semantics">Bring us the Redis! (and more divergent semantics)</h3>

<p>Then we bring Redis into the mix. With Redis, we have some prior art with locking in the form of the <a href="https://redis.io/topics/distlock">Redlock</a> algorithm, of which we won’t be needing as much. Even in our fairly high-volume use case there is no use of Redis clusters, AWS ElastiCache only provides primary and failover replica. So, let’s port our lock implementation to a single-node Redlock. Since we already are using ConnectionPool for Redis connections (and you should too) we will incorporate it directly:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">RedisIdempotencyStore</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">redis_connection_pool</span><span class="p">)</span>
    <span class="vi">@pool</span> <span class="o">=</span> <span class="n">redis_connection_pool</span>
  <span class="k">end</span>
  
  <span class="k">def</span> <span class="nf">acquire_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
    <span class="vi">@token</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">bytes</span><span class="p">(</span><span class="mi">16</span><span class="p">)</span>
    <span class="vi">@pool</span><span class="p">.</span><span class="nf">with</span> <span class="k">do</span> <span class="o">|</span><span class="n">redis</span><span class="o">|</span>
      <span class="k">return</span> <span class="n">redis</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="n">lock_key</span><span class="p">,</span> <span class="vi">@token</span><span class="p">,</span> <span class="ss">nx: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">ex: </span><span class="no">LOCK_TTL_SECONDS</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">release_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
    <span class="k">raise</span> <span class="s2">"No acquired lock"</span> <span class="k">unless</span> <span class="vi">@token</span>
    <span class="n">lua_script</span> <span class="o">=</span> <span class="o">&lt;&lt;~</span><span class="no">EOS</span><span class="sh">
      redis.replicate_commands()
      if redis.call("get",KEYS[1]) == ARGV[1] then
        -- we are still holding the lock, release it
        redis.call("del",KEYS[1])
        return "ok"
      else
        -- someone else holds the lock or it has expired
        return "stale"
      end
</span><span class="no">    EOS</span>
    <span class="vi">@pool</span><span class="p">.</span><span class="nf">with</span> <span class="k">do</span> <span class="o">|</span><span class="n">redis</span><span class="o">|</span>
      <span class="n">result</span> <span class="o">=</span> <span class="n">redis</span><span class="p">.</span><span class="nf">eval</span><span class="p">(</span><span class="n">lua_script</span><span class="p">,</span> <span class="ss">keys: </span><span class="p">[</span><span class="n">lock_key</span><span class="p">],</span> <span class="ss">argv: </span><span class="p">[</span><span class="vi">@token</span><span class="p">])</span>
      <span class="k">raise</span> <span class="s2">"Lock was lost while we held it"</span> <span class="k">unless</span> <span class="n">result</span> <span class="o">==</span> <span class="s2">"ok"</span>
    <span class="k">end</span>
    <span class="vi">@token</span> <span class="o">=</span> <span class="kp">nil</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>Note that here we have to contend with another bit of Redis semantics: since we do not have the luxury of automatic lock release if our process dies, and we do not have the luxury of automatic lock refresh - we need to set a TTL for our lock. If our process dies (and we do not manage to get to the <code class="language-plaintext highlighter-rouge">ensure</code>) our lock will still be automatically released after some time. The locking mechanism is roughly this:</p>

<ul>
  <li>Generate a random payload (the token)</li>
  <li>Set that token to the lock key in Redis, but conditionally (using SETNX). If it could be set - we acquired the lock. If it could not - someone else holds the lock.</li>
  <li>Once we are done - run a Lua script to delete the lock key. If the value of the lock key is a different token - we have lost our lock during execution (for example our execution took too long)</li>
</ul>

<p>Note that we also now have a bit of data per request, which is specific not only to the value of the <code class="language-plaintext highlighter-rouge">request_key</code> but also to our executing request - the <code class="language-plaintext highlighter-rouge">@token</code>. This token must also be protected from thread races – remember our in-memory implementation? So to facilitate that, let’s change our API from using a single datastore instance for our middleware to using a single datastore instance per request, instantiated at <code class="language-plaintext highlighter-rouge">call</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="n">datastore_factory</span><span class="p">:)</span>
  <span class="vi">@app</span> <span class="o">=</span> <span class="n">app</span>
  <span class="vi">@datastore_factory</span> <span class="o">=</span> <span class="n">datastore_factory</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
  <span class="k">return</span> <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="k">unless</span> <span class="n">env</span><span class="p">[</span><span class="s1">'HTTP_X_IDEMPOTENCY_KEY'</span><span class="p">]</span>
  <span class="n">request_key</span> <span class="o">=</span> <span class="n">compute_request_key</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>

  <span class="n">store</span> <span class="o">=</span> <span class="vi">@datastore_factory</span><span class="p">.</span><span class="nf">new</span>
  
  <span class="n">did_acquire_lock</span> <span class="o">=</span> <span class="n">store</span><span class="p">.</span><span class="nf">acquire_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
  <span class="k">return</span> <span class="p">[</span><span class="mi">429</span><span class="p">,</span> <span class="p">{},</span> <span class="p">[</span><span class="s1">'Concurrent request!'</span><span class="p">]]</span> <span class="k">unless</span> <span class="n">did_acquire_lock</span>

  <span class="n">response_triplet</span> <span class="o">=</span> <span class="n">store</span><span class="p">.</span><span class="nf">fetch_response_triplet</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
  <span class="k">return</span> <span class="n">response_triplet</span> <span class="k">if</span> <span class="n">response_triplet</span>

  <span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
  <span class="n">store</span><span class="p">.</span><span class="nf">save_response</span><span class="p">(</span><span class="n">request_key</span><span class="p">,</span> <span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span> <span class="k">if</span> <span class="n">response_idempotent?</span><span class="p">(</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span>
  <span class="p">[</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">]</span>
<span class="k">ensure</span>
  <span class="n">store</span><span class="p">.</span><span class="nf">release_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span> <span class="k">if</span> <span class="n">did_acquire_lock</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Once we do this, we can see that every single method of our datastore object wants this <code class="language-plaintext highlighter-rouge">request_key</code> and we also instantiate our datastore for that specific request key. If I remember right, this is what instance variables are very good for:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">RedisIdempotencyStore</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
    <span class="vi">@request_key</span> <span class="o">=</span> <span class="n">request_key</span>
    <span class="vi">@token</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">bytes</span><span class="p">(</span><span class="mi">16</span><span class="p">)</span>
  <span class="k">end</span>
  
  <span class="k">def</span> <span class="nf">redis_lock_key</span>
    <span class="s2">"idempotency_key_lock:</span><span class="si">#{</span><span class="vi">@request_key</span><span class="si">}</span><span class="s2">"</span>
  <span class="k">end</span>
  
  <span class="k">def</span> <span class="nf">acquire_lock</span>
    <span class="vi">@pool</span><span class="p">.</span><span class="nf">with</span> <span class="k">do</span> <span class="o">|</span><span class="n">redis</span><span class="o">|</span>
      <span class="k">return</span> <span class="n">redis</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="n">redis_lock_key</span><span class="p">,</span> <span class="vi">@token</span><span class="p">,</span> <span class="ss">nx: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">ex: </span><span class="no">LOCK_TTL_SECONDS</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">release_lock</span>
    <span class="n">lua_script</span> <span class="o">=</span> <span class="o">&lt;&lt;~</span><span class="no">EOS</span><span class="sh">
      redis.replicate_commands()
      if redis.call("get",KEYS[1]) == ARGV[1] then
        -- we are still holding the lock, release it
        redis.call("del",KEYS[1])
        return "ok"
      else
        -- someone else holds the lock or it has expired
        return "stale"
      end
</span><span class="no">    EOS</span>
    <span class="vi">@pool</span><span class="p">.</span><span class="nf">with</span> <span class="k">do</span> <span class="o">|</span><span class="n">redis</span><span class="o">|</span>
      <span class="n">result</span> <span class="o">=</span> <span class="n">redis</span><span class="p">.</span><span class="nf">eval</span><span class="p">(</span><span class="n">lua_script</span><span class="p">,</span> <span class="ss">keys: </span><span class="p">[</span><span class="n">redis_lock_key</span><span class="p">],</span> <span class="ss">argv: </span><span class="p">[</span><span class="vi">@token</span><span class="p">])</span>
      <span class="k">raise</span> <span class="s2">"Lock was lost while we held it"</span> <span class="k">unless</span> <span class="n">result</span> <span class="o">==</span> <span class="s2">"ok"</span>
    <span class="k">end</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>and our middleware code evolves like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="n">datastore_factory</span><span class="p">:)</span>
  <span class="vi">@app</span> <span class="o">=</span> <span class="n">app</span>
  <span class="vi">@datastore_factory</span> <span class="o">=</span> <span class="n">datastore_factory</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
  <span class="k">return</span> <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="k">unless</span> <span class="n">env</span><span class="p">[</span><span class="s1">'HTTP_X_IDEMPOTENCY_KEY'</span><span class="p">]</span>
  <span class="n">request_key</span> <span class="o">=</span> <span class="n">compute_request_key</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>

  <span class="n">store_for_request</span> <span class="o">=</span> <span class="vi">@datastore_factory</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
  
  <span class="n">did_acquire_lock</span> <span class="o">=</span> <span class="n">store_for_request</span><span class="p">.</span><span class="nf">acquire_lock</span>
  <span class="k">return</span> <span class="p">[</span><span class="mi">429</span><span class="p">,</span> <span class="p">{},</span> <span class="p">[</span><span class="s1">'Concurrent request!'</span><span class="p">]]</span> <span class="k">unless</span> <span class="n">did_acquire_lock</span>

  <span class="n">response_triplet</span> <span class="o">=</span> <span class="n">store_for_request</span><span class="p">.</span><span class="nf">fetch_response_triplet</span>
  <span class="k">return</span> <span class="n">response_triplet</span> <span class="k">if</span> <span class="n">response_triplet</span>

  <span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
  <span class="n">store_for_request</span><span class="p">.</span><span class="nf">save_response</span><span class="p">(</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span> <span class="k">if</span> <span class="n">response_idempotent?</span><span class="p">(</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span>
  <span class="p">[</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">]</span>
<span class="k">ensure</span>
  <span class="n">store_for_request</span><span class="p">.</span><span class="nf">release_lock</span> <span class="k">if</span> <span class="n">did_acquire_lock</span>
<span class="k">end</span>
</code></pre></div></div>

<p>We solve a few inconveniences, all in one go:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">@token</code> will always be set now, we do not have to account for <code class="language-plaintext highlighter-rouge">release_lock</code> getting called without one</li>
  <li>There will be no thread-races, as every <code class="language-plaintext highlighter-rouge">store</code> will be thread-local</li>
  <li>We do not have to pass the <code class="language-plaintext highlighter-rouge">request_key</code> around all the time</li>
</ul>

<p>but you will see that we have lost our <code class="language-plaintext highlighter-rouge">@redis_pool</code> instance variable. Since the <code class="language-plaintext highlighter-rouge">call</code> method of the middleware is going to instantiate us a datastore on every invocation, we must find a different way to shuttle the Redis connection pool into our datastore object. This requires yet another change: instead of instantiating the middleware like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">use</span> <span class="no">IdempotencyKeyMiddleware</span><span class="p">,</span> <span class="ss">datastore_factory: </span><span class="no">RedisIdempotencyStore</span>
</code></pre></div></div>

<p>we are going to change the setup and allow the datastore to be configured during initialization:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">use</span> <span class="no">IdempotencyKeyMiddleware</span><span class="p">,</span> <span class="ss">datastore: </span><span class="no">RedisIdempotencyStore</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="no">Redis</span><span class="p">.</span><span class="nf">new</span><span class="p">)</span>
</code></pre></div></div>

<p>This is a pattern I tend to use quite often - think about how you want your API to be consumed, and let that inform the implementation. To make this work, we are going to relocate our request key related activities into another object which our <code class="language-plaintext highlighter-rouge">datastore</code> can create. So, yet another change:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="n">datastore</span><span class="p">:)</span>
  <span class="vi">@app</span> <span class="o">=</span> <span class="n">app</span>
  <span class="vi">@datastore</span> <span class="o">=</span> <span class="n">datastore</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
  <span class="k">return</span> <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="k">unless</span> <span class="n">env</span><span class="p">[</span><span class="s1">'HTTP_X_IDEMPOTENCY_KEY'</span><span class="p">]</span>
  <span class="n">request_key</span> <span class="o">=</span> <span class="n">compute_request_key</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>

  <span class="n">store_for_request</span> <span class="o">=</span> <span class="vi">@datastore</span><span class="p">.</span><span class="nf">for_request</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
  
  <span class="n">did_acquire_lock</span> <span class="o">=</span> <span class="n">store_for_request</span><span class="p">.</span><span class="nf">acquire_lock</span>
  <span class="k">return</span> <span class="p">[</span><span class="mi">429</span><span class="p">,</span> <span class="p">{},</span> <span class="p">[</span><span class="s1">'Concurrent request!'</span><span class="p">]]</span> <span class="k">unless</span> <span class="n">did_acquire_lock</span>

  <span class="n">response_triplet</span> <span class="o">=</span> <span class="n">store_for_request</span><span class="p">.</span><span class="nf">fetch_response_triplet</span>
  <span class="k">return</span> <span class="n">response_triplet</span> <span class="k">if</span> <span class="n">response_triplet</span>

  <span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
  <span class="n">store_for_request</span><span class="p">.</span><span class="nf">save_response</span><span class="p">(</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span> <span class="k">if</span> <span class="n">response_idempotent?</span><span class="p">(</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span>
  <span class="p">[</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">]</span>
<span class="k">ensure</span>
  <span class="n">store</span><span class="p">.</span><span class="nf">release_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span> <span class="k">if</span> <span class="n">did_acquire_lock</span>
<span class="k">end</span>
</code></pre></div></div>

<p>and our Redis class implementation will then change again (and will actually become two classes, but the caller doesn’t care):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">RedisIdempotencyStore</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">redis_connection_pool</span><span class="p">)</span>
    <span class="vi">@pool</span> <span class="o">=</span> <span class="n">redis_connection_pool</span>
  <span class="k">end</span>
  
  <span class="k">def</span> <span class="nf">for_request</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
     <span class="no">RequestContext</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="vi">@pool</span><span class="p">,</span> <span class="n">request_key</span><span class="p">)</span>
  <span class="k">end</span>
  
  <span class="k">class</span> <span class="nc">RequestContext</span>
    <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">redis_pool</span><span class="p">,</span> <span class="n">request_key</span><span class="p">)</span>
      <span class="vi">@request_key</span> <span class="o">=</span> <span class="n">request_key</span>
      <span class="vi">@token</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">bytes</span><span class="p">(</span><span class="mi">16</span><span class="p">)</span>
      <span class="vi">@redis_pool</span> <span class="o">=</span> <span class="n">redis_pool</span>
    <span class="k">end</span>
    
    <span class="k">def</span> <span class="nf">redis_lock_key</span>
      <span class="s2">"idempotency_key_lock:</span><span class="si">#{</span><span class="vi">@request_key</span><span class="si">}</span><span class="s2">"</span>
    <span class="k">end</span>
    
    <span class="k">def</span> <span class="nf">acquire_lock</span>
      <span class="vi">@pool</span><span class="p">.</span><span class="nf">with</span> <span class="k">do</span> <span class="o">|</span><span class="n">redis</span><span class="o">|</span>
        <span class="k">return</span> <span class="n">redis</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="n">redis_lock_key</span><span class="p">,</span> <span class="vi">@token</span><span class="p">,</span> <span class="ss">nx: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">ex: </span><span class="no">LOCK_TTL_SECONDS</span><span class="p">)</span>
      <span class="k">end</span>
    <span class="k">end</span>
  
    <span class="k">def</span> <span class="nf">release_lock</span>
      <span class="n">lua_script</span> <span class="o">=</span> <span class="o">&lt;&lt;~</span><span class="no">EOS</span><span class="sh">
        redis.replicate_commands()
        if redis.call("get",KEYS[1]) == ARGV[1] then
          -- we are still holding the lock, release it
          redis.call("del",KEYS[1])
          return "ok"
        else
          -- someone else holds the lock or it has expired
          return "stale"
        end
</span><span class="no">      EOS</span>
      <span class="vi">@pool</span><span class="p">.</span><span class="nf">with</span> <span class="k">do</span> <span class="o">|</span><span class="n">redis</span><span class="o">|</span>
        <span class="n">result</span> <span class="o">=</span> <span class="n">redis</span><span class="p">.</span><span class="nf">eval</span><span class="p">(</span><span class="n">lua_script</span><span class="p">,</span> <span class="ss">keys: </span><span class="p">[</span><span class="n">redis_lock_key</span><span class="p">],</span> <span class="ss">argv: </span><span class="p">[</span><span class="vi">@token</span><span class="p">])</span>
        <span class="k">raise</span> <span class="s2">"Lock was lost while we held it"</span> <span class="k">unless</span> <span class="n">result</span> <span class="o">==</span> <span class="s2">"ok"</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>and similar for other store implementations. Once that is tackled, we end up with another peculiar race condition, which has been well described by Martin Kleppmann in his rebuttal of the Redlock algorithm: namely, the lock TTL can expire while the concurrent request is still in progress. This is why we raise an exception in our <code class="language-plaintext highlighter-rouge">release_lock</code> method - if we do not hold the lock we should signal it somehow. The timeline for the lock getting lost looks like this:</p>

<p><img src="/assets/idempo/race-doublereq.png" alt="Lock expires while a request is still processing" /></p>

<p>We have two “bad things” which can happen:</p>

<ul>
  <li>A concurrent request is going to be allowed through (see the figure above)</li>
  <li>Cached response is going to be overwritten, with an unspecified request winning the write. A “stolen write” will look like this:</li>
</ul>

<p><img src="/assets/idempo/race-doublesave.png" alt="Request overwrites saved response in a race condition" /></p>

<p>One of those we can only prevent by using a more robust locking service. If you have a database with advisory locks - great! you can use it, but you will need to contend with expiring stored responses manually. But if you want to truck along with Redis - and Redis is a very, very juicy tool for this kind of usage - we will need to do some <em>more</em> work, all to satisfy the semantics. What we are going to do is add a pre-condition, which we are going to define as follows:</p>

<p><em>The cached response may only be written to the datastore as long as the lock is still held by this particular request.</em> Thus…</p>

<h3 id="bring-us-the-lock-and-response-persistense-synchronization">Bring us the lock and response persistense synchronization!</h3>

<p>Instead of doing just this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">save_response</span><span class="p">(</span><span class="n">response_triplet</span><span class="p">)</span>
  <span class="n">marshaled_response</span> <span class="o">=</span> <span class="n">serialize_response</span><span class="p">(</span><span class="n">response_triplet</span><span class="p">)</span>
  <span class="vi">@pool</span><span class="p">.</span><span class="nf">with</span> <span class="k">do</span> <span class="o">|</span><span class="n">redis</span><span class="o">|</span>
    <span class="n">redis</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="n">redis_stored_response_key</span><span class="p">,</span> <span class="n">marshaled_response</span><span class="p">,</span> <span class="ss">ex: </span><span class="n">ttl_for_stored_responses</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>we will have to do this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">save_response</span><span class="p">(</span><span class="n">response_triplet</span><span class="p">)</span>
  <span class="n">marshaled_response</span> <span class="o">=</span> <span class="n">serialize_response</span><span class="p">(</span><span class="n">response_triplet</span><span class="p">)</span>
  <span class="vi">@pool</span><span class="p">.</span><span class="nf">with</span> <span class="k">do</span> <span class="o">|</span><span class="n">redis</span><span class="o">|</span>
    <span class="n">held_lock</span> <span class="o">=</span> <span class="n">redis</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">redis_lock_key</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">held_lock</span> <span class="o">==</span> <span class="vi">@token</span>
      <span class="n">redis</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="n">redis_stored_response_key</span><span class="p">,</span> <span class="n">marshaled_response</span><span class="p">,</span> <span class="ss">ex: </span><span class="n">ttl_for_stored_responses</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>We check whether the token stored in the lock key still is the one that belongs to us, and if it is - we allow the save. But here be dragons again - we just introduced another race condition. If the lock key gets expired (or deleted) between our call to <code class="language-plaintext highlighter-rouge">redis.get</code> and <code class="language-plaintext highlighter-rouge">redis.set</code> we will potentially overwrite someone else’s response. So yet again we need to revert to using Redis scripts to guarantee atomicity. Enter more Lua:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">save_response</span><span class="p">(</span><span class="n">response_triplet</span><span class="p">)</span>
  <span class="n">set_with_ttl_if_lock_held_lua_script</span> <span class="o">=</span> <span class="o">&lt;&lt;~</span><span class="no">EOS</span><span class="sh">
    redis.replicate_commands()
    if redis.call("get", KEYS[1]) == ARGV[1] then
      -- we are still holding the lock, we can go ahead and set it
      redis.call("set", KEYS[2], ARGV[2], "px", ARGV[3])
      return "ok"
    else
      return "stale"
    end
</span><span class="no">  EOS</span>

  <span class="vi">@pool</span><span class="p">.</span><span class="nf">with</span> <span class="k">do</span> <span class="o">|</span><span class="n">redis</span><span class="o">|</span>
    <span class="n">redis</span><span class="p">.</span><span class="nf">eval</span><span class="p">(</span><span class="n">set_with_ttl_if_lock_held_lua_script</span><span class="p">,</span> <span class="ss">keys: </span><span class="p">[</span><span class="n">redis_lock_key</span><span class="p">,</span> <span class="n">redis_stored_response_key</span><span class="p">],</span> <span class="ss">argv: </span><span class="p">[</span><span class="vi">@token</span><span class="p">,</span> <span class="n">marshaled_response</span><span class="p">])</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Remember how I said that <strong>coordinating the lock and the saving/loading of the cached response will be necessary</strong> earlier in the article? Here it is then: the two actually have to be managed together, because the persistence of the response body must be coherent with the locks, otherwise data corruption can occur.</p>

<p>So, from our former list, we have fixed 2 races out of 3:</p>

<ul>
  <li>Our request loses the lock, another concurrent request starts, and we write out our cached response before the other request does - <strong>Fixed</strong></li>
  <li>Our request loses the lock, another concurrent request starts, and it writes its cached response out which we then overwrite - <strong>Fixed</strong></li>
  <li>Our request loses the lock, another concurrent request starts, and neither write out the cached response because the race condition gets detected <strong>Undefined</strong></li>
</ul>

<p>We are not able to prevent the concurrent request from starting, but we <em>are</em> able to prevent its side effect from being persisted - which is already a win. For the concurrent request the only sensible thing to do is to <strong>document that in case of a lost lock a concurrent request may be started</strong> and move on. Really: we can bikeshed about this for a while, but like I mentioned previously – here we are either <strong>correct</strong> or <strong>not correct</strong> and for the latter, it is a much better choice to warn the user of the software upfront.</p>

<h3 id="the-final-api">The final API</h3>

<p>After all of the above, we end up with something like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
   <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="k">unless</span> <span class="n">idempotent_request_and_idempotency_key?</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
   <span class="n">request_key</span> <span class="o">=</span> <span class="n">extract_request_key</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
   <span class="vi">@datastore</span><span class="p">.</span><span class="nf">with_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">store</span><span class="o">|</span>
      <span class="k">return</span> <span class="n">stored</span> <span class="k">if</span> <span class="n">stored</span> <span class="o">=</span> <span class="n">store</span><span class="p">.</span><span class="nf">lookup</span>
      <span class="n">response</span> <span class="o">=</span> <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
      <span class="n">store</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
   <span class="k">end</span>
<span class="k">rescue</span> <span class="no">ConcurrentRequest</span>
  <span class="p">[</span><span class="mi">409</span><span class="p">,</span> <span class="p">{},</span> <span class="p">[]]</span>
<span class="k">end</span>
</code></pre></div></div>

<p>and the key module which we need to implement it would look approximately like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">DataStore</span>
  <span class="k">def</span> <span class="nf">with_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
    <span class="n">did_acquire</span> <span class="o">=</span> <span class="n">acquire_lock</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
    <span class="k">raise</span> <span class="no">ConcurrentRequest</span> <span class="k">unless</span> <span class="n">did_acquire</span>
    <span class="n">store_for_this_request</span> <span class="o">=</span> <span class="no">Store</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span>
    <span class="k">yield</span><span class="p">(</span><span class="n">store_for_this_request</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">class</span> <span class="nc">Store</span> <span class="o">&lt;</span> <span class="no">Struct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:request_key</span><span class="p">)</span>
    <span class="k">def</span> <span class="nf">lookup</span>
      <span class="c1"># ...some code to retrieve the triplet for the request</span>
    <span class="k">end</span>
    
    <span class="k">def</span> <span class="nf">save</span><span class="p">(</span><span class="n">status_headers_and_body</span><span class="p">)</span>
      <span class="c1"># ...some code to save the triplet for the request</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">DataStore</code> implementation can then be swapped to use different persistence mechanisms / systems. We hold by the following desirable properties:</p>

<ol>
  <li>There is only one place where we have to pass our <code class="language-plaintext highlighter-rouge">request_key</code> to the data store (good)</li>
  <li>Everything that is supposed to happen while the lock is held goes into the block (good)</li>
  <li>We use exceptions to signal a concurrent request, so we can implement one “divergent path” for the entire flow (good)</li>
  <li>We did set up an API which is not obvious (bad). But hopefully this article explains the choices made.</li>
  <li>Serializing and unserializing the response is somehow the responsibility of the datastore (bad), let’s fix that:</li>
</ol>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Store</span> <span class="o">&lt;</span> <span class="no">Struct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:request_key</span><span class="p">)</span>
  <span class="k">def</span> <span class="nf">lookup</span>
    <span class="c1"># ...some code to retrieve the marshaled binary string with response</span>
  <span class="k">end</span>
  
  <span class="k">def</span> <span class="nf">save</span><span class="p">(</span><span class="n">marshaled_binary_string_with_response</span><span class="p">)</span>
    <span class="c1"># ...some code to save the marshaled response</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<ol>
  <li>We also need to be able to set the TTL for our saved response and it is not available yet. Let’s fix that:</li>
</ol>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Store</span> <span class="o">&lt;</span> <span class="no">Struct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:request_key</span><span class="p">)</span>
  <span class="k">def</span> <span class="nf">lookup</span>
    <span class="c1"># ...some code to retrieve the marshaled binary string with response</span>
  <span class="k">end</span>
  
  <span class="k">def</span> <span class="nf">save</span><span class="p">(</span><span class="n">marshaled_binary_string_with_response</span><span class="p">:,</span> <span class="n">ttl_seconds</span><span class="p">:)</span>
    <span class="c1"># ...some code to save the marshaled response with expiry</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="how-problematic-are-the-stale-locks">How problematic are the stale locks?</h3>

<p>Needless to say, we <em>did</em> get some of those stale locks in production too! At WeTransfer scale, anything that <em>may</em> happen undoubtedly <em>will</em> happen. These cases were few and far between (maybe one in a million requests). Were they critical for us? No, not really. Would they have been critical for us if our requests were doing this?</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
   <span class="n">params</span> <span class="o">=</span> <span class="n">parse_params</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
   <span class="n">account</span> <span class="o">=</span> <span class="no">Account</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
   <span class="n">account</span><span class="p">.</span><span class="nf">debit</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:amount</span><span class="p">])</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Of course it would, at which point…</p>

<h3 id="you-still-have-to-pick-your-poison">You still have to pick your poison</h3>

<p>If we rely on the idempotency keys to prevent a double <code class="language-plaintext highlighter-rouge">debit</code> we would then have a choice between using the database store and lock (for more guarantees, and less performance) or using the Redis store and lock, and then add, say, database locks just for this case. If we haven’t had the code for using advisory locks already in the application (which we did) we could have used <a href="https://github.com/ClosureTree/with_advisory_lock">with_advisory_lock</a> as a supplement for our idempotency key <em>just</em> in this case:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
   <span class="n">params</span> <span class="o">=</span> <span class="n">parse_params</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
   <span class="n">account</span> <span class="o">=</span> <span class="no">Account</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
   <span class="n">ik</span> <span class="o">=</span> <span class="n">env</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s1">'HTTP_IDEMPOTENCY_KEY'</span><span class="p">)</span>
   <span class="no">Account</span><span class="p">.</span><span class="nf">with_advisory_lock</span><span class="p">(</span><span class="n">ik</span><span class="p">)</span> <span class="k">do</span>
     <span class="n">account</span><span class="p">.</span><span class="nf">debit</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:amount</span><span class="p">])</span>
   <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Or we could make our own <code class="language-plaintext highlighter-rouge">store</code> implementation, and use the database for locks and Redis for storing responses.</p>

<h3 id="it-aint-easy">It ain’t easy</h3>

<p>No, <a href="https://www.youtube.com/watch?v=aK7xSsgDx2I">really</a> - there are quite a few other things in <code class="language-plaintext highlighter-rouge">idempo</code> where even more work was required. I just outlined the locks as the most critical part of it all, and how the design evolved to satisfy the locking paradigms of various datastores. Just to name a few other items:</p>

<ul>
  <li>Saved responses have to expire after some time. At the minimum, with DB-drive persistence we have to have an index on <code class="language-plaintext highlighter-rouge">created_at</code> to do bulk expiries.</li>
  <li>The called app should be able to control how long the cached response gets persisted for (we added a header for that)</li>
  <li>Saving responses only makes sense when the response size is not too big for the datastore. Do you want to save a 1GB ZIP file response?</li>
  <li>Serializing responses should use something that does not break when Ruby versions change (so no <code class="language-plaintext highlighter-rouge">Marshal</code>) yet it must support binary data (so no JSON and no YAML either). We had to settle on Messagepack in the end</li>
  <li>Stored responses do not have to be aggregated, and APIs produce responses that compress very well. Responses are thus deflated on save</li>
  <li>Saving a Rack response needs reading the Rack response body. And a Rack response body might be non-rewindable, so when we do cache we have to replace the response body we return with an Array of cached strings. This is also done <em>only</em> if the response can be sized ahead of time and small enough.</li>
  <li>Since idempotency keys are actually a “poor man’s version” of <code class="language-plaintext highlighter-rouge">ETag</code>-based caching, some extra headers of the request need to be mixed into the <code class="language-plaintext highlighter-rouge">request_key</code> - for example the <code class="language-plaintext highlighter-rouge">Authorization</code> header, otherwise you can expose yourself to some pretty serious exploits.</li>
  <li>Some datastores take TTLs in seconds, some take them in milliseconds. What is the lowest common denominator?.. etc.</li>
  <li>An app must be permitted to opt out of response caching entirely (some of our responses have signed URLs, those have limited TTL themselves <em>and</em> we want them to be fresh)</li>
  <li>We need to implement advisory locks for PostgreSQL too - and the lock key there is a signed bigint, not a string.</li>
</ul>

<p>For example, here is the main call from <code class="language-plaintext highlighter-rouge">idempo</code> in its entirety (compare to the chunks above):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">return</span> <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="k">if</span> <span class="n">request_verb_idempotent?</span><span class="p">(</span><span class="n">req</span><span class="p">)</span>
  <span class="k">return</span> <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="k">unless</span> <span class="n">idempotency_key_header</span> <span class="o">=</span> <span class="n">extract_idempotency_key_from</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>

  <span class="c1"># The RFC requires that the Idempotency-Key header value is enclosed in quotes</span>
  <span class="n">idempotency_key_header</span> <span class="o">=</span> <span class="n">unquote</span><span class="p">(</span><span class="n">idempotency_key_header</span><span class="p">)</span>
  <span class="k">raise</span> <span class="no">MalformedIdempotencyKey</span> <span class="k">if</span> <span class="n">idempotency_key_header</span> <span class="o">==</span> <span class="s1">''</span>

  <span class="n">fingerprint</span> <span class="o">=</span> <span class="n">compute_request_fingerprint</span><span class="p">(</span><span class="n">req</span><span class="p">)</span>
  <span class="n">request_key</span> <span class="o">=</span> <span class="s2">"</span><span class="si">#{</span><span class="n">idempotency_key_header</span><span class="si">}</span><span class="s2">_</span><span class="si">#{</span><span class="n">fingerprint</span><span class="si">}</span><span class="s2">"</span>

  <span class="vi">@backend</span><span class="p">.</span><span class="nf">with_idempotency_key</span><span class="p">(</span><span class="n">request_key</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">store</span><span class="o">|</span>
    <span class="k">if</span> <span class="n">stored_response</span> <span class="o">=</span> <span class="n">store</span><span class="p">.</span><span class="nf">lookup</span>
      <span class="no">Measurometer</span><span class="p">.</span><span class="nf">increment_counter</span><span class="p">(</span><span class="s1">'idempo.responses_served_from'</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="ss">from: </span><span class="s1">'store'</span><span class="p">)</span>
      <span class="k">return</span> <span class="n">from_persisted_response</span><span class="p">(</span><span class="n">stored_response</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">response_may_be_persisted?</span><span class="p">(</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span>
      <span class="n">expires_in_seconds</span> <span class="o">=</span> <span class="p">(</span><span class="n">headers</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="s1">'X-Idempo-Persist-For-Seconds'</span><span class="p">)</span> <span class="o">||</span> <span class="no">DEFAULT_TTL</span><span class="p">).</span><span class="nf">to_i</span>
      <span class="c1"># Body is replaced with a cached version since a Rack response body is not rewindable</span>
      <span class="n">marshaled_response</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="n">serialize_response</span><span class="p">(</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span>
      <span class="n">store</span><span class="p">.</span><span class="nf">store</span><span class="p">(</span><span class="ss">data: </span><span class="n">marshaled_response</span><span class="p">,</span> <span class="ss">ttl: </span><span class="n">expires_in_seconds</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="no">Measurometer</span><span class="p">.</span><span class="nf">increment_counter</span><span class="p">(</span><span class="s1">'idempo.responses_served_from'</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="ss">from: </span><span class="s1">'freshly-generated'</span><span class="p">)</span>
    <span class="p">[</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">body</span><span class="p">]</span>
  <span class="k">end</span>
</code></pre></div></div>

<h3 id="in-conclusion">In conclusion</h3>

<p>We did finish and deploy it, and it works really, really well. It took us 2 throwaway implementations before we arrived at this one, and a few more trials to pick the best datastore. Maybe with a story like this it can be more insightful to see what it <em>really</em> takes to build a good library, and how an API comes to be designed. We do not publish enough details about how software gets designed, which makes it less likely that our software will be found and used. I don’t want this to happen to idempo. Making a gem which does a seemingly tiny thing can be devilishly complex, and switchable implementations for things are actually useful. So: hope you enjoyed the ride, go forth and use idempo 🤗</p>

<p>And when you come to design a gem, write down some traveler’s diaries for us all to enjoy too. We in general do not give enough attention to how software gets designed, and that’s a shame.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I’ve already touched on it a bit in the article about doing the scariest thing first – one of the things we managed to do at WeTransfer before I left was implementing proper idempotency keys for our storage management system (called Storm). The resulting gem is called idempo and you use it about like this: config.middleware.insert_after Rack::Head, Idempo, backend: Idempo::RedisBackend.new(Redis.new) It is great and you should try it out. If you are pressed for time, TL;DR: we built a gem for idempotency keys in Rack applications. It was way harder than we expected, and we could not find an existing one. As a community we do not publish enough details about how software gets designed, which makes it less likely that our software will be found and used. I don’t want this to happen to idempo. Making a gem which does a seemingly tiny thing can be devilishly complex, and switchable implementations for things are actually useful. Disclaimer: consider all code here to be pseudocode. For actual working versions of the same check out the code in idempo itself. Why did we even need it? To recap: idempotency keys allow you to reject double requests to modify the same resource (or to apply the same modification to the same resource), and they map pretty nicely both to REST HTTP endpoints and to RPC endpoints. Normally idempotency keys are implemented using a header. For a good exposition on idempotency keys, check out the two articles by Brandur Leach here - the first one gives a nice introduction, and the second one gives a much more actionable set of guidelines for implementing one. The point where we realised that we will need idempotency keys in the first place came about when we decided to let other teams use a JavaScript module that we would provide. The module - and the upload protocol WeTransfer uses - is peculiar in that it has quite a bit of implicit state. Multiple requests are necessary, and they need to be synchronised somewhat carefully. Requests should be retried, because we were already using a lot of autoscaling - so a server could end up dying during a request. Yet some of the operations we let our JS client perform (such as creating a new transfer) must be atomic - you can only create a transfer once, and there is some bookkeeping involved when doing that. The transfer is going to have a pre-assigned ID, and if the client attempts to create a transfer and then does not register properly that the transfer got created the ID will end up taken. This bookkeeping touches the database, and thus creates database load. Also, the output of those operations can be cached for some time. In the past, we had situations where an uploader would end up in an endless loop (due to problems with retry logic for example) and would hit the same endpoint, near-endlessly, and very frequently. If we had an idempotency key system we could significantly reduce the impact this had on our systems – and avoid a number of production incidents. So with the new JS client we wanted to make it support an idempotency key for the entire upload process for your transfer, and we wanted to have this idempotency key be transparently used on the server. As a matter of fact, also our iOS app ended up implementing idempotency keys in the same way - and with the same benefits. Surprisingly to us, while Ilja Eftimov has made a good write up about idempotency keys and made a demo of an implementation in this article we were surprised to find no proper gems for idempotency keys existed, which we could pick off the shelf. So some brainstorming and a little pondering later we decided that we had to make one, albeit only for our storage manager system. It is not that Ilja’s code is bad – it just omits a few interesting side-effects which might be more frequent than we could think of initially. This article is long, and there are a few things I want to touch on here. Before we move any further: idempo came about with great help from Lorenzo Grandi and Pablo Crivella, sending my hugs to both. Lorenzo is also in the fabulous new Honeypot documentary that you can find here.]]></summary></entry><entry><title type="html">The value of not having to be right</title><link href="https://blog.julik.nl/2021/12/the-value-of-not-having-to-be-right" rel="alternate" type="text/html" title="The value of not having to be right" /><published>2021-12-21T00:00:00+00:00</published><updated>2021-12-21T00:00:00+00:00</updated><id>https://blog.julik.nl/2021/12/the-value-of-not-having-to-be-right</id><content type="html" xml:base="https://blog.julik.nl/2021/12/the-value-of-not-having-to-be-right"><![CDATA[<p>In software we pride ourselves in being “data-informed”, “metrics driven”, and “formally proven” is the highest praise. Few things feel as satisfying as being <em>actually</em> right, with no shadow of a doubt and no way of escape for our opponents. Being tech people, we cling to this idea that “the more correct” idea, or the one which is “objectively right”, should win.</p>

<p>Now, do not underestimate this:</p>

<blockquote>
  <p>As long as all we have is opinions, mine is the best.</p>
</blockquote>

<p>which is “one way to do it” – specifically, “a way” to do it if the team is comprised of jerks. But believe it or not - most teams are composed of decent humans who genuinely want to do good by each other.</p>

<hr><p>I am currently available for contract work.
              <a href="https://blog.julik.nl/hire-me">Hire me</a>to help make your Rails app better!
            </p><hr><!--more-->

<p>The problem I have with the “prove everything” approach is that there are side-effects to it. First is this: switching gears into “proofs” also switches gears into the “slow thinking mode”. Making decisions - but also inventing a solution - will then become a much more deliberate, much more “solemn” process. And altough the delivered solution might, indeed, be marginally better than all the others, the cost of that marginal improvement – of “doing things right” – will be the extra time spent on the “proof rituals”. Some questions - and some problems - <em>do</em> in fact demand that rigor, but most of the run-of-the-mill problems that we encounter in web-app land actually do not. By indiscriminately demanding proofs for insignificant things we rob our teams of time and of agency.</p>

<p>The second side-effect is the possibility to endlessly bikeshed as long as nobody on the team comes about bringing exhaustive evidence which trumps all the others. The corpus of data requirement is assumed to be scientific, but it omits a few important differences: teams develop software with deadlines, with implicit expectations and within certain political pressures put on them. Actual scientific studies sometimes end up disproving their original hypothesis. They sometimes fail altogether, because the equipment or the methods turn out to be invalid or unusable for the purpose. Evaluating and challenging each other on the merits of the work is a standard feature in academia, but scientists often have a luxury that teams in product development – especially teams in startups – do not posess. That luxury is the abundance of time. A ritual of proving and refuting might eat into the time budget allotted to the team for actually performing, and can lead to a delay that may kill a product - doing nothing, for lack of proof that <em>something</em> could work, can be more deadly than delivering <em>something</em> that could keep the team moving ahead.</p>

<p>The third side-effect is “distrust by default”. Teams where everyone must be “right by the numbers” create a culture where any opinion or emotion is “null and void” by default unless based on a corpus of data. This creates a few dynamics which you really ought to anticipate for:</p>

<ul>
  <li>Folks withhold their ideas until they have accumulated enough “proof” to allow themselves to feel safe enough to share them with the group, to be “worthy of being heard”. In the extremes people stop contributing or talking completely as they know they will never have sufficient proof. Partly because…</li>
  <li><a href="https://www.ribbonfarm.com/2015/05/28/the-amazing-shrinking-org-chart/">Flat hierarchies are fake.</a> There will be a person who also has to “prove themselves to be right”, but just ever-so-slightly less vigorously. Maybe they are the CTO, or one of the co-founders, or the most senior developer on the team. Maybe they have hoarded access to actual resources needed during delivery (like production access), or maybe they are de-facto the only on-call for the system to be built (so while they might not be the one contributing the most into the design they will be hit the hardest once the shit hits the fan). The end result will be that for <em>that person</em> it will be permissible to “drive by opinion” while the rest of the team will have to scramble for proof. Or to “be right” ever so slightly less.</li>
</ul>

<p>These exclude people and erode trust.</p>

<p>Yet another item to perish in proof-addicted teams is experimentation. In many, many areas there absolutely is space for trying stuff out. But if the smallest of actions have to be substantiated with proofs, indiscriminately, the threshold for experimentation will be raised. And this is where creative work becomes a challenge. See, a good team is expected (it is never explicit, but often implied) to produce “miracles” from time to time. A sudden discovery or two, a nice optimisation, a hackathon project or another… Squeeze hard enough with the proof requirements, and you either suffocate experimentation entirely, or reserve it for the select few who know how to come with the right proofs at the right time (or are the entitled people mentioned above).</p>

<blockquote>
  <p>Caveat: none of the above applies when you are Alphabet and you can permit yourself to have dedicated research teams composed entirely of PhDs. Or if you are Apple and you are creating a compiler team for a new programming language. At those points you are doing exactly the different thing - you are doing science! Congratulations, because it is unlikely your team of PhD’s is going to be tasked with delivering a product, cadence over cadence, on a very tight deadline and with dozens of very minute choices. When doing science, different constraints apply.</p>
</blockquote>

<p>Where is “proof of being right” really justified? Well, for instance, where a very specific technical choice with knowable outcomes is being made. When the consequences of that choice are going to be dramatic and impactful, and you are absolutely not in a position to mess it up. It might be in the scope of technical leadership to actually cherry-pick the issues which should be study-level proven for correctness, because to execute on them the team wil have to go into the slow thinking mode.</p>

<p>Or when there is a relatively small choice which can be easily proven with a small verification step - like a choice of a datastructure. For example: “using a map here instead of using a list makes searches with 1000+ items 10x faster in this particular UI, here are the numbers”.</p>

<p>Now, when you are in a situation when a team is already doing that <em>too much</em> there might be a benefit to breaking out of the vicious circle of everyone being exhausted of endlessly proving (on insignificant details). There are discussions which get stalled completely due to the “demands of the right way”. There are a few opportunities to drive the discussion back into a more humane process, at least for less critical topics. Here are a few questions which can help defuse these situations.</p>

<p>One is evaluating  the cost of making the “wrong” choice, potentially on all contested items. What would be the cost of the wrong choice? Not to the individual proposing/critiquing but to the product? To the team? To the business? For instance: “What is the worst thing that could happen if we pick this technology or use this particular style of writing tests? And we realise it is wrong? What is going to be the cost of rework? Will we violate user privacy? Are we likely to get sued? Are we likely to run out of budget?”</p>

<p>Another is deliberately driving for the emotional component of the desire to “do it this way” to be brought to the fore, using tools from non-violent communication. It ain’t easy, and participants have to be vulnerable and open for feedback, and “crucial conversations” and “you must be this tall” - but you won’t get actual trust without going to those dark places. This can be difficult if folks on the team are very insecure, and if they have been conditioned to get hurt when they express subjective judgment. But it still can work and have a great healing effect. For example, consider an exchange like this:</p>

<p>– Why doing it this way, using <em>thing,</em> make you feel bad?<br />
– Well, this is going to make using a slow query list much less predictable, so it violates an assumption.<br />
– Fair point, do we use the slow query list in our system?<br />
– No, but we might want to in the future!<br />
– Ok, and if we use <em>thing,</em> and need to accomodate the use of the slow query list later - how much work will it be? Will you be OK with me committing to doing this work if we need it? (commitment can be stated in writing, for example in commit messages or in issue trackers)<br />
– This might work, but I rather us just not do this thing.<br />
– Why do you feel that way? Your feeling is probably based on an experience you have had, could you share it with me so that I can understand your struggle better?<br />
– Well, at that previous place I was at we used the slow query list all the time. See, we had a lot of people doing queries, and a lot of them would hang or suddenly run for a very long time - so we used the query list all the time to kill the slowest queries. It was not possible to tell people to examine the queries they were executing - there was never enough time, and the teams they were on were incentivised not to optimize queries because there was a feature churn. And I was one of the people who had to use the query list to kill slow queries, sometimes a few times a day! That was awful…<br />
– I understand better now. Given that we all know the importance of not shipping slow queries, and we are all aware of what happens when those queries need to be manually aborted - can we do the implementation with that knowledge, and leave good documentation in place so that we know where to look? Will that make you feel less bad about the fact that we need to introduce <em>thing</em>?<br />
– Yes, this will work!</p>

<p>Nowhere in that exchange is it possible to establish numeric merit. Specifically: would using <em>thing</em> be more kilosomethings worth to us than having better use of the slow query list? In most discussions we won’t know ahead of time. The relative merit of those two things might even change for us at some point later. Either using <em>thing</em> or mandating that everything must be done in service of the slow query list could be “right” here, depending on the context. If we switch the discussion to figure out why the participants have certain preferences in the first place we can establish a much better causality:</p>

<ul>
  <li>Person A wants to introduce <em>thing</em></li>
  <li>Person B is afraid of <em>thing</em> because it might subtly break <em>another thing</em> and the person was previously hurt because that <em>another thing</em> was extremely critical to them</li>
  <li>The hesitation Person B expresses is not grounded in the current business need <em>or</em> in using the right tool for the job <em>or</em> in using the right solution in general – it is grounded in personal experience and trauma. We cannot include Person B meaningfully without having that trauma in the picture, and we can’t make a good choice that makes both parties happy without acknowledging and processing that trauma.</li>
</ul>

<p>So with a little empathy we can remove the requirement of “being right”. The idea is to work out a mutual understanding of the fact that it is actually <em>ok</em> to have opinions - we just have to be more transparent about where those opinions come from, and we have to be open to yielding. It is OK to have opinions which are not “completely right”. It is OK to be subjective sometimes, and one does not need to bring the entirety of the decade’s USENIX catalogue just to be heard.</p>

<p>There are times to dig up that catalogue, but we can move much faster if we reserve it for special occasions.</p>

<p>See also – <a href="http://agileotter.blogspot.com/2019/09/is-it-my-fault-you-cant-handle-truth.html">Is It My Fault You Can’t Handle The Truth?</a></p>]]></content><author><name></name></author><summary type="html"><![CDATA[In software we pride ourselves in being “data-informed”, “metrics driven”, and “formally proven” is the highest praise. Few things feel as satisfying as being actually right, with no shadow of a doubt and no way of escape for our opponents. Being tech people, we cling to this idea that “the more correct” idea, or the one which is “objectively right”, should win. Now, do not underestimate this: As long as all we have is opinions, mine is the best. which is “one way to do it” – specifically, “a way” to do it if the team is comprised of jerks. But believe it or not - most teams are composed of decent humans who genuinely want to do good by each other.]]></summary></entry><entry><title type="html">Art, science, taste and “clean code”</title><link href="https://blog.julik.nl/2021/11/art-science-taste-and-clean-code" rel="alternate" type="text/html" title="Art, science, taste and “clean code”" /><published>2021-11-22T00:00:00+00:00</published><updated>2021-11-22T00:00:00+00:00</updated><id>https://blog.julik.nl/2021/11/art-science-taste-and-clean-code</id><content type="html" xml:base="https://blog.julik.nl/2021/11/art-science-taste-and-clean-code"><![CDATA[<p>Science establishes concepts that describe nature, and is often able to signal binary answers to questions. “Can acceleration be non-0 when velocity is 0?” “What is the circumference of a circle?” “How many chromosomes does a fruit fly genome contain?”</p>

<p>Art, unlike science, speaks to our emotion. Great art is great exactly because - in addition to execution - it stimulates us to imagine something which makes us feel in a certain way. It is about communicating emotion.</p>

<p>There is a ton of talk about how “bikeshedding details” is “sophistry”, “you should not care that much”, “style reviews create opportunities for abuse” and the like. But we are, as a community, slowly moving towards optimising for two things, and two things alone:</p>

<ul>
  <li>Making all changes we do measurable improvements. Either using objective or fake <em>metrics</em> which will somehow demonstrate that “we were right” or “we were wrong”</li>
  <li>Making nobody feel bad, ever</li>
</ul>

<p>When we optimize in that direction, we tend to dismiss (or even discourage) “taste”, because of course it is personal, it is subjective, and it can be imposed by someone in position of authority. What we do skimp on in the process, is that “bikeshedding” design decisions - and code! - bringing back taste thus - can produce a solution which is not only “nicer”, or “pleases the loudest senior person on the team the most”. There are things we can debate in that domain, and they are all of differing orders:</p>

<ul>
  <li>Not code formatting (just install an automatic formatter for this and move on)</li>
  <li>Size of modules / functions</li>
  <li>Granularity of modules / functions</li>
  <li>Verbosity / DRYness of tests</li>
  <li>Quality of encapsulation</li>
</ul>

<p>While the things above are not quanitifiable, the paradox is that their <em>outcomes</em> can be, or at the very list they can be <em>qualifiable.</em> They <em>are</em> important and if you give them some TLC you <em>are</em> going to get reductions in your cost of ownership down the line.</p>

<h3 id="the-good-questions-for-bikeshedding">The good questions for bikeshedding</h3>

<p>Here are those, and I was incredibly lucky to see more than a few times when prioritizing them in bikeshedding discussions led to meaningful, useful outcomes. I like to formulate them as <em>questions</em> - because barking orders at each other is exactly what creates the toxic environments we overcorrected from. Let’s walk through those questions:</p>

<ul>
  <li>How long will it take a person who never worked on your module before to read your test when there is a problem? What will be the hurdles they are going to likely encounter? What will be the cost of unpacking the abstractions you have used?</li>
  <li>What could we change so that the addition of your module, in total, allows us to <em>have less software?</em></li>
  <li>Is there something in your change that is going to be difficult to understand for a person 1 level below you in seniority? 2 levels? 3 levels?</li>
  <li>If this codebase already contains 3 places where a similar module/change has been added, does your 4th change warrant doing in a different style? Are you committing for the other 3 too or are you just being a passenger for this one feature?</li>
  <li>What will be the cost of removing this module you are adding? Can we reduce the necessary churn it to removing 2 files (module + module test) from the code repository? and have nothing break?</li>
  <li>How many jumps from module to module (or function to function) will someone have to do to understand a specific flow in its entirety?</li>
  <li>Does the API surface of this module map well onto the underlying system one level down that it is driving?</li>
</ul>

<p>Case study: if you ever wondered why so many have problems with Redux, try to size the codebases using Redux that you have seen against this list of questions:</p>

<ul>
  <li>How hard will it be to remove this reducer+actions+dispatch functions if we want to get rid of them?</li>
  <li>How much indirection has to be followed to read this UX flow start-to-finish?</li>
  <li>Is the use of Redux state coherent with the use of local state?</li>
</ul>

<h3 id="questions-map-to-costs">Questions map to costs</h3>

<p>In effect, when we bikeshed over <em>these</em> questions, we optimize for two very specific <em>costs</em> of software to us:</p>

<ul>
  <li>Cost of reading and understanding</li>
  <li>Cost of removal/rework</li>
</ul>

<p>And these costs are also to the business, because they will be very apparent when features have to change, or when the teams need to scale. Let’s deal with those in order.</p>

<h3 id="cost-of-reading-and-understanding">Cost of reading and understanding</h3>

<p>The first one is essential, and also something that is not well covered either in vocational study (bootcamps) or in CS curricula - we spend way, way more time reading and understanding existing code than we do creating new code. We absolutely <em>do not</em> pay enough attention to making our code easier to understand. And making code easier to read and understand is directly coupled to those pesky “taste” and “style” issues we so so forbid each other from discussing. Just a small sampling of those:</p>

<ul>
  <li>Longer identifiers (<code class="language-plaintext highlighter-rouge">max_width</code> instead of <code class="language-plaintext highlighter-rouge">mw</code>)</li>
  <li>Identifiers hinting behavior or type (<code class="language-plaintext highlighter-rouge">maybe_user</code> for a nullable, <code class="language-plaintext highlighter-rouge">body_str</code> for a string as opposed to “body abstraction from one of the libraries we use”)</li>
  <li>Use of keyword arguments/named arguments over positional arguments (<code class="language-plaintext highlighter-rouge">insert(at: pos, item: it)</code> over <code class="language-plaintext highlighter-rouge">insert(it, pos)</code></li>
  <li>Use of standard language constructs over framework constructs (<code class="language-plaintext highlighter-rouge">prepend</code> over <code class="language-plaintext highlighter-rouge">ActiveSupport::Concern</code>)</li>
  <li>Comments explaining any non-obvious behavior (<code class="language-plaintext highlighter-rouge"># S3 multipart part numbers are 1-based</code>)</li>
  <li>Metaprogramming / macro output examples next to macro code</li>
</ul>

<p>And these questions - if you look close enough - are not of the variety “I like it more” - they are of the variety “we are not doing our job well because it will be harder for a new person to understand this system”.</p>

<p>If we follow the now-mainstream “make everyone feel nice” ideology, we are invariably getting to a situation where <em>asking</em> for these affordances becomes a social misstep.</p>

<p>Moreover: modern teams with high-paced delivery operate via very, very opaque socio-political streams. With how hard it is to “perform” in a modern enterprise getting the “code” right is actually the <em>easy</em> part! There is a whole battery of adverse effects of the modern workplace which are going to make it impossible for the same person to “own” the same module for any meaningful amount of time. But exactly <em>because</em> of these difficulties we should pay more attention. Even if the model of operation is “commit the module, have people get their promotion, have a reorg, be moved to the next feature” - someone is going to inherit this code and highly likely will have to deal with it in some way. Someone <em>will</em> carry your can. The faster our org chart iteration, the more important it is to make your material discoverable, readable, clear.</p>

<h3 id="cost-of-removalrework">Cost of removal/rework</h3>

<p>This is something we do not think about much at all, because “removing a piece of software never got anyone promoted” - just like “nobody got fired for choosing Java”. But it does provide tangible benefits, and does make iteration easier!</p>

<p>For example, in the last project I have worked on, we implemented idempotency keys. Despite two great articles on the topic existing - one from Brandur and another from Ilja - there was no good module for idempotency keys we could use off-the-shelf, so we had to roll our own. We had to go through 2 throwaway implementations before we found one that became <a href="https://github.com/julik/idempo">idempo</a></p>

<p>This would have been considerably harder to do if our idempotency keys were managed from the various applications we have inside of our Rack wrapper application, and became very easy with just one line of middleware. To swapover from one implementation to another, we had to change 2 lines in our codebase. To remove an iteration which didn’t work, we had to delete 2 files and 2 directories (since we used modules, everything could be removed in one go).</p>

<p>Same for things where - if you squint well enough - you say “if we were aiming for the microservice architecture this module would be a service”. Why not make it a single module with one function? If the fashion for microservices stays, and the product you are working on becomes more successful, replacing a local function call with an RPC call will be easy. Going in the opposite direction will be much harder because <em>the cost of removal</em> of a microservice is higher (remember the bit about “delete 2 files”).</p>

<p>See also - <a href="https://programmingisterrible.com/post/139222674273/how-to-write-disposable-code-in-large-systems">Write code that is easy to delete, not easy to extend.</a></p>

<p><strong>Good kind of bikeshedding is bikeshedding which optimizes for better communication and easy removal</strong>. Let me leave you with this quote by @zverok which should be printed on banners and hung on walls across all the offices where software gets worked on:</p>

<p><img src="/assets/clean-code/zverok.png" alt="Zverok" /></p>

<p>Truly the whole thread is magnificient - find it <a href="https://twitter.com/zverok/status/1405231797139554304">here</a></p>

<p>Thus: the bike shed should be green, because most bikesheds in our neighbourhood are green and because we regularly hire people who have never in their life seen a bike shed. And it must use keyword arguments. No argument about it.</p>

<p>For another great and considerate take on the topic - see <a href="https://sandimetz.com/blog/2017/6/1/why-we-argue-style">Why We Argue: Style</a> by Sandi Metz.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Science establishes concepts that describe nature, and is often able to signal binary answers to questions. “Can acceleration be non-0 when velocity is 0?” “What is the circumference of a circle?” “How many chromosomes does a fruit fly genome contain?” Art, unlike science, speaks to our emotion. Great art is great exactly because - in addition to execution - it stimulates us to imagine something which makes us feel in a certain way. It is about communicating emotion. There is a ton of talk about how “bikeshedding details” is “sophistry”, “you should not care that much”, “style reviews create opportunities for abuse” and the like. But we are, as a community, slowly moving towards optimising for two things, and two things alone: Making all changes we do measurable improvements. Either using objective or fake metrics which will somehow demonstrate that “we were right” or “we were wrong” Making nobody feel bad, ever When we optimize in that direction, we tend to dismiss (or even discourage) “taste”, because of course it is personal, it is subjective, and it can be imposed by someone in position of authority. What we do skimp on in the process, is that “bikeshedding” design decisions - and code! - bringing back taste thus - can produce a solution which is not only “nicer”, or “pleases the loudest senior person on the team the most”. There are things we can debate in that domain, and they are all of differing orders: Not code formatting (just install an automatic formatter for this and move on) Size of modules / functions Granularity of modules / functions Verbosity / DRYness of tests Quality of encapsulation While the things above are not quanitifiable, the paradox is that their outcomes can be, or at the very list they can be qualifiable. They are important and if you give them some TLC you are going to get reductions in your cost of ownership down the line. The good questions for bikeshedding Here are those, and I was incredibly lucky to see more than a few times when prioritizing them in bikeshedding discussions led to meaningful, useful outcomes. I like to formulate them as questions - because barking orders at each other is exactly what creates the toxic environments we overcorrected from. Let’s walk through those questions: How long will it take a person who never worked on your module before to read your test when there is a problem? What will be the hurdles they are going to likely encounter? What will be the cost of unpacking the abstractions you have used? What could we change so that the addition of your module, in total, allows us to have less software? Is there something in your change that is going to be difficult to understand for a person 1 level below you in seniority? 2 levels? 3 levels? If this codebase already contains 3 places where a similar module/change has been added, does your 4th change warrant doing in a different style? Are you committing for the other 3 too or are you just being a passenger for this one feature? What will be the cost of removing this module you are adding? Can we reduce the necessary churn it to removing 2 files (module + module test) from the code repository? and have nothing break? How many jumps from module to module (or function to function) will someone have to do to understand a specific flow in its entirety? Does the API surface of this module map well onto the underlying system one level down that it is driving? Case study: if you ever wondered why so many have problems with Redux, try to size the codebases using Redux that you have seen against this list of questions: How hard will it be to remove this reducer+actions+dispatch functions if we want to get rid of them? How much indirection has to be followed to read this UX flow start-to-finish? Is the use of Redux state coherent with the use of local state? Questions map to costs In effect, when we bikeshed over these questions, we optimize for two very specific costs of software to us: Cost of reading and understanding Cost of removal/rework And these costs are also to the business, because they will be very apparent when features have to change, or when the teams need to scale. Let’s deal with those in order. Cost of reading and understanding The first one is essential, and also something that is not well covered either in vocational study (bootcamps) or in CS curricula - we spend way, way more time reading and understanding existing code than we do creating new code. We absolutely do not pay enough attention to making our code easier to understand. And making code easier to read and understand is directly coupled to those pesky “taste” and “style” issues we so so forbid each other from discussing. Just a small sampling of those: Longer identifiers (max_width instead of mw) Identifiers hinting behavior or type (maybe_user for a nullable, body_str for a string as opposed to “body abstraction from one of the libraries we use”) Use of keyword arguments/named arguments over positional arguments (insert(at: pos, item: it) over insert(it, pos) Use of standard language constructs over framework constructs (prepend over ActiveSupport::Concern) Comments explaining any non-obvious behavior (# S3 multipart part numbers are 1-based) Metaprogramming / macro output examples next to macro code And these questions - if you look close enough - are not of the variety “I like it more” - they are of the variety “we are not doing our job well because it will be harder for a new person to understand this system”. If we follow the now-mainstream “make everyone feel nice” ideology, we are invariably getting to a situation where asking for these affordances becomes a social misstep. Moreover: modern teams with high-paced delivery operate via very, very opaque socio-political streams. With how hard it is to “perform” in a modern enterprise getting the “code” right is actually the easy part! There is a whole battery of adverse effects of the modern workplace which are going to make it impossible for the same person to “own” the same module for any meaningful amount of time. But exactly because of these difficulties we should pay more attention. Even if the model of operation is “commit the module, have people get their promotion, have a reorg, be moved to the next feature” - someone is going to inherit this code and highly likely will have to deal with it in some way. Someone will carry your can. The faster our org chart iteration, the more important it is to make your material discoverable, readable, clear. Cost of removal/rework This is something we do not think about much at all, because “removing a piece of software never got anyone promoted” - just like “nobody got fired for choosing Java”. But it does provide tangible benefits, and does make iteration easier! For example, in the last project I have worked on, we implemented idempotency keys. Despite two great articles on the topic existing - one from Brandur and another from Ilja - there was no good module for idempotency keys we could use off-the-shelf, so we had to roll our own. We had to go through 2 throwaway implementations before we found one that became idempo This would have been considerably harder to do if our idempotency keys were managed from the various applications we have inside of our Rack wrapper application, and became very easy with just one line of middleware. To swapover from one implementation to another, we had to change 2 lines in our codebase. To remove an iteration which didn’t work, we had to delete 2 files and 2 directories (since we used modules, everything could be removed in one go). Same for things where - if you squint well enough - you say “if we were aiming for the microservice architecture this module would be a service”. Why not make it a single module with one function? If the fashion for microservices stays, and the product you are working on becomes more successful, replacing a local function call with an RPC call will be easy. Going in the opposite direction will be much harder because the cost of removal of a microservice is higher (remember the bit about “delete 2 files”). See also - Write code that is easy to delete, not easy to extend. Good kind of bikeshedding is bikeshedding which optimizes for better communication and easy removal. Let me leave you with this quote by @zverok which should be printed on banners and hung on walls across all the offices where software gets worked on: Truly the whole thread is magnificient - find it here Thus: the bike shed should be green, because most bikesheds in our neighbourhood are green and because we regularly hire people who have never in their life seen a bike shed. And it must use keyword arguments. No argument about it. For another great and considerate take on the topic - see Why We Argue: Style by Sandi Metz.]]></summary></entry><entry><title type="html">Do the scariest thing first</title><link href="https://blog.julik.nl/2021/11/do-the-scariest-thing-first" rel="alternate" type="text/html" title="Do the scariest thing first" /><published>2021-11-11T00:00:00+00:00</published><updated>2021-11-11T00:00:00+00:00</updated><id>https://blog.julik.nl/2021/11/do-the-scariest-thing-first</id><content type="html" xml:base="https://blog.julik.nl/2021/11/do-the-scariest-thing-first"><![CDATA[<blockquote>
  <p>I know the pieces fit, cause I watched them fall apart</p>
</blockquote>

<p>Kir recently wrote about <a href="https://kirshatrov.com/posts/fragmented-prototyping/">fragmented prototyping</a> which struck a nerve. I use a very similar approach for gnarly engineering and system design problems, so figured I could share while we are at it. I call it <em>the maximum pain upfront</em> approach. Another name could be <em>do the thing that scares you the most first.</em></p>

<p>It goes roughly like this. When you need to design a system, make an inventory of the tasks/challenges you expect and make a list of them. Preferably list out all of them, in detail. Then look at that list, and find the thing you know the least about - or a thing that scares you the most. Then try to “run around” your system and design the least possible amount of “glue” around the piece you are worried about. It doesn’t have to be perfect, “just enough” is enough. Make your system do something sensible, provide just that little bit of output which proves your system is sane and can roughly do what it is supposed to do. This will be your “skeleton”.</p>

<p>Then comes the “maximum fear” part. Laser-focus on the part of the system which scares you the most. Something you never done before. Something you do not know the constraints of. Something that requires you going 2-3 levels “down” from what you normally consider comfortable. I’ll give a few examples from my experience where I hit those “maximum fear” aspects:</p>

<ul>
  <li>In <a href="https://github.com/WeTransfer/zip_tricks">zip_tricks</a> the part I was completely lost about was parallel compression. In retrospect it wasn’t that necessary because we ended up not using it (the cost of speculatively compressing all of the uploaded data proved too great when more than 80% uses lossy compression formats, and will thus not compress well)</li>
  <li>In <a href="https://github.com/julik/sylens">SyLens</a> it was matrix transforms</li>
  <li>In the download server, Ruby memory consumption was the item that scared me the most - especially because initial experiments were not encouraging at all</li>
  <li>In <a href="https://github.com/julik/idempo">idempo</a> the atomicity guarantees and data races were legitimately scaring me quite a bit</li>
</ul>

<p>Here is how to recognise one:</p>

<ul>
  <li>You never did anything like this before.</li>
  <li>You vaguely know that the thing will have behavior you do not completely understand - like race conditions, or numeric precision issues</li>
  <li>You are unable to visualise the code / design which will carry the feature ahead of time</li>
  <li>You know that the problem touches a theory topic you are unfamiliar with, and it will require you to upskill</li>
  <li>You try to search for readymade solutions, and they are vaguely close – but don’t look like “closed form” solutions, or you have doubts you wil be able to use them in your case</li>
</ul>

<p>In all of these situations, I went about it roughly the same:</p>

<ul>
  <li>Set up the “skeleton”, or “harness”, which will run the code that scares you the most - it can either be a minimum possible implementation of the “system around” that you are building, or just a test runner. Make sure it works first, make it accept input and provide some output! These “scary problems” often cause you to backtrack - or, in extreme cases, will need the work abandoned! Be prepared for this and do not get caught with a blank sheet of paper. Having a “skeleton” will allow you some holdfast to come back to if you get lost in the woods</li>
  <li>Try any approaches that could work. Test-driven might work well if you can isolate the part of the system well. Metrics work great - profile and measure and try to compare various solutions to give you perspective. Use visualisations, sketches, Matlab plots, Jupyter notebooks, Excel - anything that gets you closer to a solution is fair game! Doesn’t have to be in the target language even (sometimes).</li>
  <li>Time-box your effort. This is important! Scary problems are nerdsnipe nirvana. You can become so consumed that you will lose track of all the other work that needs to be done. If you do not have a time limit a scary problem can consume you for <em>months</em> and you won’t even notice.</li>
</ul>

<p>In my <a href="https://www.imdb.com/name/nm3127224/">previous work</a> I had a few jobs where I would laser-focus on a particular part of the job, partially because it was the chunk I was unsure about the most. I would then spend most of my allotted time – without the “skeleton” setup in place – on “nailing” the part I was fearful of. As the time ran out, I would en up in a situation where only about 20% of the “scary thing” was done, but there was nothing besides it. No skeleton, no reversion strategy and no “big strokes” version to backtrack to. This was very embarrassing and painful for the clients/stakeholders too!</p>

<p>So, imagine you end up with a problem similar to what Kir encountered: “Make programming language X do Y bytes per second over interface Z.” You have never done this before, you know that this requires <em>detail work</em> and it might yield a negative outcome (“it is not possible to make this thing do that”). Time-box for it! Allow yourself a day or two just for that problem, and have a wrapper in place. If you fail, you can backtrack to the wrapper and begin again, or replace your yet-missing implementation with a shim of some kind. It will also give you space to try again later. On some of these problems, I had to make 2, 3 or even 4 attempts before the final solution emerged.</p>

<p>A few caveats are in order of course.</p>

<ul>
  <li>When you see a problem like this - think about bypassing it outright. For example: you know that DynamoDB has write and read quotas, and you are afraid of hitting them. Think about whether your project <em>needs</em> DynamoDB to begin with. Could you do without it? Maybe using a datastore with different guarantees and tradeoffs can get you to the end solution faster, and will allow you to skip the problem?</li>
  <li>What will you do if you are unable to solve the problem? Have a plan B. If your system is moot without the scary component: congratulations, you have potentially blocked yourself.</li>
  <li>Have a buddy. You will, after some time in the field, have a list of names you could scroll through – of people who “know a lot about X”. Ask the person who knows the most about the part that scares you so much, maybe not immediately but it helps to have them in the back of your mind. This is where bona-fide networking becomes essential.</li>
  <li>This is not necessary everywhere. Sometimes there is no “scary component”, there is just… grind. Time pressure, temperamental client/stakeholder, shitty deployments, that sort of thing. Know to recognise and manage accordingly. Especially when we get bored out of our mind, we tend to create complicated contraptions, fight through their complexities and then admire the end result, while they should not have been applied in the project to begin with.</li>
  <li>The technique is usable when you can recognise the scary problem. It is going to be much, much harder for a junior to recognise those – and they are different for each and every person (there is an intersection of your skillset and problems you can attack). A crucial task for a mentor is to find those scary parts ahead of time, and either steer the mentee around them or try to attack them ahead of time to give the mentee some cover.</li>
  <li>And obviously: the bigger the team, the easier it would be to find people familiar with the topic of the scary problem. They might be able to crack it for you quickly – so divide work if you can. If your team can have good, hot, frank conversations about areas of expertise: you are in luck.</li>
  <li>It could be that the true challenge lies where you did not expect it instead. By investing time in the scariest thing you wil rob yourself of time that you could have used for discovering the “unknowns” you didn’t even think existed, and they would turn out even scarier. So again: the “scariest thing upfront” technique I would recommend to experienced users.</li>
</ul>

<p>This approach has saved my bacon quite a few times. Use with moderation, and may you always succeed.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I know the pieces fit, cause I watched them fall apart Kir recently wrote about fragmented prototyping which struck a nerve. I use a very similar approach for gnarly engineering and system design problems, so figured I could share while we are at it. I call it the maximum pain upfront approach. Another name could be do the thing that scares you the most first. It goes roughly like this. When you need to design a system, make an inventory of the tasks/challenges you expect and make a list of them. Preferably list out all of them, in detail. Then look at that list, and find the thing you know the least about - or a thing that scares you the most. Then try to “run around” your system and design the least possible amount of “glue” around the piece you are worried about. It doesn’t have to be perfect, “just enough” is enough. Make your system do something sensible, provide just that little bit of output which proves your system is sane and can roughly do what it is supposed to do. This will be your “skeleton”. Then comes the “maximum fear” part. Laser-focus on the part of the system which scares you the most. Something you never done before. Something you do not know the constraints of. Something that requires you going 2-3 levels “down” from what you normally consider comfortable. I’ll give a few examples from my experience where I hit those “maximum fear” aspects: In zip_tricks the part I was completely lost about was parallel compression. In retrospect it wasn’t that necessary because we ended up not using it (the cost of speculatively compressing all of the uploaded data proved too great when more than 80% uses lossy compression formats, and will thus not compress well) In SyLens it was matrix transforms In the download server, Ruby memory consumption was the item that scared me the most - especially because initial experiments were not encouraging at all In idempo the atomicity guarantees and data races were legitimately scaring me quite a bit Here is how to recognise one: You never did anything like this before. You vaguely know that the thing will have behavior you do not completely understand - like race conditions, or numeric precision issues You are unable to visualise the code / design which will carry the feature ahead of time You know that the problem touches a theory topic you are unfamiliar with, and it will require you to upskill You try to search for readymade solutions, and they are vaguely close – but don’t look like “closed form” solutions, or you have doubts you wil be able to use them in your case In all of these situations, I went about it roughly the same: Set up the “skeleton”, or “harness”, which will run the code that scares you the most - it can either be a minimum possible implementation of the “system around” that you are building, or just a test runner. Make sure it works first, make it accept input and provide some output! These “scary problems” often cause you to backtrack - or, in extreme cases, will need the work abandoned! Be prepared for this and do not get caught with a blank sheet of paper. Having a “skeleton” will allow you some holdfast to come back to if you get lost in the woods Try any approaches that could work. Test-driven might work well if you can isolate the part of the system well. Metrics work great - profile and measure and try to compare various solutions to give you perspective. Use visualisations, sketches, Matlab plots, Jupyter notebooks, Excel - anything that gets you closer to a solution is fair game! Doesn’t have to be in the target language even (sometimes). Time-box your effort. This is important! Scary problems are nerdsnipe nirvana. You can become so consumed that you will lose track of all the other work that needs to be done. If you do not have a time limit a scary problem can consume you for months and you won’t even notice. In my previous work I had a few jobs where I would laser-focus on a particular part of the job, partially because it was the chunk I was unsure about the most. I would then spend most of my allotted time – without the “skeleton” setup in place – on “nailing” the part I was fearful of. As the time ran out, I would en up in a situation where only about 20% of the “scary thing” was done, but there was nothing besides it. No skeleton, no reversion strategy and no “big strokes” version to backtrack to. This was very embarrassing and painful for the clients/stakeholders too! So, imagine you end up with a problem similar to what Kir encountered: “Make programming language X do Y bytes per second over interface Z.” You have never done this before, you know that this requires detail work and it might yield a negative outcome (“it is not possible to make this thing do that”). Time-box for it! Allow yourself a day or two just for that problem, and have a wrapper in place. If you fail, you can backtrack to the wrapper and begin again, or replace your yet-missing implementation with a shim of some kind. It will also give you space to try again later. On some of these problems, I had to make 2, 3 or even 4 attempts before the final solution emerged. A few caveats are in order of course. When you see a problem like this - think about bypassing it outright. For example: you know that DynamoDB has write and read quotas, and you are afraid of hitting them. Think about whether your project needs DynamoDB to begin with. Could you do without it? Maybe using a datastore with different guarantees and tradeoffs can get you to the end solution faster, and will allow you to skip the problem? What will you do if you are unable to solve the problem? Have a plan B. If your system is moot without the scary component: congratulations, you have potentially blocked yourself. Have a buddy. You will, after some time in the field, have a list of names you could scroll through – of people who “know a lot about X”. Ask the person who knows the most about the part that scares you so much, maybe not immediately but it helps to have them in the back of your mind. This is where bona-fide networking becomes essential. This is not necessary everywhere. Sometimes there is no “scary component”, there is just… grind. Time pressure, temperamental client/stakeholder, shitty deployments, that sort of thing. Know to recognise and manage accordingly. Especially when we get bored out of our mind, we tend to create complicated contraptions, fight through their complexities and then admire the end result, while they should not have been applied in the project to begin with. The technique is usable when you can recognise the scary problem. It is going to be much, much harder for a junior to recognise those – and they are different for each and every person (there is an intersection of your skillset and problems you can attack). A crucial task for a mentor is to find those scary parts ahead of time, and either steer the mentee around them or try to attack them ahead of time to give the mentee some cover. And obviously: the bigger the team, the easier it would be to find people familiar with the topic of the scary problem. They might be able to crack it for you quickly – so divide work if you can. If your team can have good, hot, frank conversations about areas of expertise: you are in luck. It could be that the true challenge lies where you did not expect it instead. By investing time in the scariest thing you wil rob yourself of time that you could have used for discovering the “unknowns” you didn’t even think existed, and they would turn out even scarier. So again: the “scariest thing upfront” technique I would recommend to experienced users. This approach has saved my bacon quite a few times. Use with moderation, and may you always succeed.]]></summary></entry></feed>