<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="https://www.yellowduck.be/pretty-atom-feed-v3.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <link href="https://www.yellowduck.be" rel="alternate"/>
  <link href="https://www.yellowduck.be/posts/feed" rel="self"/>
  <author>
    <name>Pieter Claerhout</name>
    <email>pieter@yellowduck.be</email>
  </author>
  <id>https://www.yellowduck.be/posts/feed</id>
  <title>🐥 YellowDuck.be</title>
  <updated>2026-06-02T17:00:00Z</updated>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/why-crypto-getrandomvalues-matters-in-javascript" rel="alternate"/>
    <content type="html">&lt;p&gt;Generating random values sounds simple, until you need randomness that is actually secure.&lt;/p&gt;
&lt;p&gt;A lot of JavaScript developers reach for &lt;code&gt;Math.random()&lt;/code&gt; out of habit. While that works fine for visual effects, games, or non-critical IDs, it should never be used for anything security-sensitive.&lt;/p&gt;
&lt;p&gt;That’s where the Web Crypto API comes in.&lt;/p&gt;
&lt;h1&gt;The problem with &lt;code&gt;Math.random()&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;Math.random()&lt;/code&gt; is not cryptographically secure.&lt;/p&gt;
&lt;p&gt;Its output is deterministic and predictable enough that an attacker may be able to reproduce or guess generated values under certain conditions.&lt;/p&gt;
&lt;p&gt;That makes it unsuitable for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Session tokens&lt;/li&gt;
&lt;li&gt;Password reset links&lt;/li&gt;
&lt;li&gt;API keys&lt;/li&gt;
&lt;li&gt;CSRF tokens&lt;/li&gt;
&lt;li&gt;Encryption keys&lt;/li&gt;
&lt;li&gt;Secure identifiers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;36&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This may look random, but it is not secure.&lt;/p&gt;
&lt;h1&gt;Using &lt;code&gt;crypto.getRandomValues()&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;Modern browsers provide a secure random number generator through the Web Crypto API.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;variable&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getRandomValues&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;variable-builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This fills the typed array with cryptographically secure random bytes provided by the operating system.&lt;/p&gt;
&lt;h1&gt;Generating a secure random token&lt;/h1&gt;
&lt;p&gt;A common use case is generating secure tokens or identifiers.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;generateToken&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getRandomValues&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;byte&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;padStart&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;0&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;variable-builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;generateToken&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example output:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;4f8b7d3a1f9e0c8d7a2b6c5d4e3f1a9c
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is significantly safer than using &lt;code&gt;Math.random()&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;Typed arrays are required&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;crypto.getRandomValues()&lt;/code&gt; only works with integer-based typed arrays.&lt;/p&gt;
&lt;p&gt;Supported examples include:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;Uint16Array&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;Int32Array&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will fail:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;variable&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getRandomValues&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because regular JavaScript arrays are not supported.&lt;/p&gt;
&lt;h1&gt;Browser and runtime support&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;crypto.getRandomValues()&lt;/code&gt; is widely supported in modern browsers.&lt;/p&gt;
&lt;p&gt;It is also available in runtimes like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node.js&lt;/li&gt;
&lt;li&gt;Deno&lt;/li&gt;
&lt;li&gt;Bun&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example in Node.js:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;variable&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getRandomValues&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;variable-builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In older Node.js versions, developers typically used:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-builtin&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;crypto&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;randomBytes&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;UUID generation&lt;/h1&gt;
&lt;p&gt;If your goal is generating UUIDs, modern runtimes also support:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;variable&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;550e8400-e29b-41d4-a716-446655440000
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Internally, this also uses cryptographically secure randomness.&lt;/p&gt;
&lt;h1&gt;Things to remember&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;Math.random()&lt;/code&gt; for non-security-related randomness only&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;crypto.getRandomValues()&lt;/code&gt; for anything security-sensitive&lt;/li&gt;
&lt;li&gt;Prefer &lt;code&gt;crypto.randomUUID()&lt;/code&gt; when generating UUIDs&lt;/li&gt;
&lt;li&gt;Always generate randomness using the operating system’s secure RNG&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For modern JavaScript applications, &lt;code&gt;crypto.getRandomValues()&lt;/code&gt; should be the default choice whenever security matters.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/javascript&quot;&gt;#javascript&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/auth&quot;&gt;#auth&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-06-02T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/why-crypto-getrandomvalues-matters-in-javascript</id>
    <title>🐥 Why crypto.getRandomValues() matters in JavaScript</title>
    <updated>2026-06-02T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/how-to-find-all-jira-issues-ever-assigned-to-someone-even-historical-ones" rel="alternate"/>
    <content type="html">&lt;p&gt;If you&apos;ve ever tried to pull up all the tickets that &lt;strong&gt;Sarah Mitchell&lt;/strong&gt; worked on last year, you&apos;ve probably started with the obvious query:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;assignee = &quot;sarah.mitchell@example.com&quot;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then realised it only shows her &lt;em&gt;current&lt;/em&gt; tickets — not the ones she handed off to &lt;strong&gt;Tom Bergkamp&lt;/strong&gt; or closed six months ago.&lt;/p&gt;
&lt;p&gt;Here&apos;s the fix.&lt;/p&gt;
&lt;h1&gt;The &lt;code&gt;was&lt;/code&gt; operator&lt;/h1&gt;
&lt;p&gt;Jira&apos;s JQL has a &lt;code&gt;was&lt;/code&gt; operator that checks the &lt;strong&gt;change history&lt;/strong&gt; of a field, not just its current value. So to find every issue Sarah was ever assigned to:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;assignee was &quot;sarah.mitchell@example.com&quot;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simple, but powerful.&lt;/p&gt;
&lt;h1&gt;Checking multiple people at once&lt;/h1&gt;
&lt;p&gt;Need to audit the work of an entire sub-team? Use &lt;code&gt;was in&lt;/code&gt; with a list:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;assignee was in (&quot;sarah.mitchell@example.com&quot;, &quot;tom.bergkamp@example.com&quot;, &quot;priya.nair@example.com&quot;)
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This returns any issue that was assigned to &lt;em&gt;any&lt;/em&gt; of those three people at any point in time.&lt;/p&gt;
&lt;h1&gt;Scoping to a specific year&lt;/h1&gt;
&lt;p&gt;Combine it with the &lt;code&gt;during&lt;/code&gt; clause to limit results to a time window — useful for annual reviews, retrospectives, or handover audits:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;assignee was in (&quot;sarah.mitchell@example.com&quot;, &quot;tom.bergkamp@example.com&quot;) during (&quot;2025-01-01&quot;, &quot;2025-12-31&quot;)
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This only matches issues where one of them was the assignee &lt;em&gt;at some point during 2025&lt;/em&gt; — even if the ticket has since been reassigned or closed.&lt;/p&gt;
&lt;h1&gt;Putting it all together&lt;/h1&gt;
&lt;p&gt;A full, practical query might look like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;project = &quot;PLATFORM&quot;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;AND assignee was in (&quot;sarah.mitchell@example.com&quot;, &quot;tom.bergkamp@example.com&quot;)
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;AND during (&quot;2025-01-01&quot;, &quot;2025-12-31&quot;)
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;ORDER BY updated DESC
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;was&lt;/code&gt; operator works on other fields too — &lt;code&gt;status was &quot;In Progress&quot;&lt;/code&gt;, &lt;code&gt;priority was &quot;High&quot;&lt;/code&gt;, etc. — so it&apos;s worth keeping in your JQL toolkit whenever you need to query history rather than current state.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-31T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/how-to-find-all-jira-issues-ever-assigned-to-someone-even-historical-ones</id>
    <title>🐥 How to find all Jira issues ever assigned to someone (even historical ones)</title>
    <updated>2026-05-31T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/downloading-external-images-as-squares-from-a-phoenix-app" rel="alternate"/>
    <content type="html">&lt;p&gt;A common need in admin tools: click a button, download a remote image as a square JPEG. Simple enough — until CORS gets in the way.&lt;/p&gt;
&lt;h1&gt;The CORS problem&lt;/h1&gt;
&lt;p&gt;When fetching a cross-origin image and drawing it onto a &lt;code&gt;&lt;canvas&gt;&lt;/code&gt;, the browser marks the canvas as &quot;tainted&quot;. The moment you call &lt;code&gt;canvas.toBlob()&lt;/code&gt; to read the pixel data back out, it throws a security error. Unless the image server sends explicit CORS headers — which most don&apos;t — you can&apos;t do canvas operations on cross-origin images.&lt;/p&gt;
&lt;h1&gt;The fix: a server-side proxy&lt;/h1&gt;
&lt;p&gt;The solution is to proxy the image through the Phoenix app. From the browser&apos;s point of view the image comes from the same origin, so the canvas stays clean.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;image_proxy&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;url&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;-&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;allowed_url?&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;       &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;-&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Req&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;content_type&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;      &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;headers&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;image/jpeg&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;      &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;      &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;hd&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;put_resp_content_type&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;content_type&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;send_resp&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;else&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;boolean&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;send_resp&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Invalid URL&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;send_resp&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;502&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Failed to fetch image&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;allowed_url?/1&lt;/code&gt; function validates the host against a known allowlist — an important SSRF guard so the proxy can&apos;t be abused to fetch arbitrary internal URLs.&lt;/p&gt;
&lt;h1&gt;Center-cropping to a square in the browser&lt;/h1&gt;
&lt;p&gt;Once the image loads from the proxy, a small Canvas API snippet handles the crop. The logic is straightforward: use the shorter dimension as the square size, then offset into the longer dimension by half the difference to take the center slice.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;variable-builtin&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;phx:download-square-image&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;filename&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;detail&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;proxyUrl&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;`/image-proxy?url=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;function-builtin&quot;&gt;encodeURIComponent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;img&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;Image&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method&quot;&gt;onload&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;naturalWidth&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;naturalHeight&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;canvas&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;canvas&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;width&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;height&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;ctx&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getContext&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;2d&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;offsetX&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;naturalWidth&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;offsetY&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;naturalHeight&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;drawImage&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;offsetX&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;offsetY&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;toBlob&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;      &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;a&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;      &lt;span class=&quot;variable&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;href&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;createObjectURL&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;      &lt;span class=&quot;variable&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;download&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;filename&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;      &lt;span class=&quot;variable&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;      &lt;span class=&quot;constant&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;revokeObjectURL&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;image/jpeg&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0.95&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;src&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;proxyUrl&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a landscape image the left and right edges are trimmed, keeping the center. For a portrait image the top and bottom are trimmed instead. The full shorter dimension is always preserved — no upscaling, no padding.&lt;/p&gt;
&lt;h1&gt;Wiring it up in LiveView&lt;/h1&gt;
&lt;p&gt;The download is triggered from the template using Phoenix&apos;s &lt;code&gt;JS.dispatch/2&lt;/code&gt;, which fires a custom DOM event with the image URL and filename as detail:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-heex&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;button&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;tag-attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;cursor-pointer&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;tag-attribute&quot;&gt;phx-click&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;module&quot;&gt;JS&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;phx:download-square-image&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;detail: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;image_url&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;filename: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;image.jpg&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;tag-delimiter&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  Download
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;&lt;/span&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No LiveView round-trip needed — &lt;code&gt;JS.dispatch&lt;/code&gt; fires the event directly in the browser, the &lt;code&gt;window&lt;/code&gt; listener catches it, and the download happens entirely client-side after the one proxy fetch.&lt;/p&gt;
&lt;h1&gt;Why &lt;code&gt;naturalWidth&lt;/code&gt; instead of &lt;code&gt;width&lt;/code&gt;?&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;img.width&lt;/code&gt; returns the CSS-rendered size, which is meaningless for an image that isn&apos;t attached to the DOM. &lt;code&gt;img.naturalWidth&lt;/code&gt; always returns the actual pixel dimensions of the image data — the right value to use when doing pixel-level canvas operations.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/javascript&quot;&gt;#javascript&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/frontend&quot;&gt;#frontend&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-29T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/downloading-external-images-as-squares-from-a-phoenix-app</id>
    <title>🐥 Downloading external images as squares from a Phoenix app</title>
    <updated>2026-05-29T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/the-tailwind-enabled-selector-trick-for-disabled-buttons" rel="alternate"/>
    <content type="html">&lt;p&gt;When you add a &lt;code&gt;disabled&lt;/code&gt; attribute to a &lt;code&gt;&lt;button&gt;&lt;/code&gt; element, you probably expect it to just... look disabled. But there&apos;s a subtle trap that catches a lot of developers: your &lt;code&gt;hover:&lt;/code&gt; styles still apply visually, even when the button is disabled.&lt;/p&gt;
&lt;h1&gt;The problem&lt;/h1&gt;
&lt;p&gt;Consider a typical Tailwind button:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-html&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;bg-gray-200 hover:bg-gray-300 hover:cursor-pointer&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  Click me
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even though the button is &lt;code&gt;disabled&lt;/code&gt; and won&apos;t fire any events, the &lt;code&gt;hover:bg-gray-300&lt;/code&gt; and &lt;code&gt;hover:cursor-pointer&lt;/code&gt; classes still apply on hover. The cursor becomes a pointer and the background changes — giving the user a false signal that the button is interactive.&lt;/p&gt;
&lt;h1&gt;The fix: &lt;code&gt;enabled:&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;Tailwind ships with an &lt;code&gt;enabled:&lt;/code&gt; variant that maps directly to the CSS &lt;code&gt;:enabled&lt;/code&gt; pseudo-class. Swap your &lt;code&gt;hover:&lt;/code&gt; styles for &lt;code&gt;enabled:hover:&lt;/code&gt; and the problem disappears:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-html&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;bg-gray-200 enabled:hover:bg-gray-300 enabled:hover:cursor-pointer&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  Click me
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now those styles only apply when the button is &lt;strong&gt;not&lt;/strong&gt; disabled. No JavaScript, no conditional class logic, no extra wrapper — just a single variant prefix.&lt;/p&gt;
&lt;h1&gt;Why this matters&lt;/h1&gt;
&lt;p&gt;The &lt;code&gt;:enabled&lt;/code&gt; pseudo-class is the semantic opposite of &lt;code&gt;:disabled&lt;/code&gt;. It&apos;s supported in all modern browsers and has been in CSS for years, but it&apos;s easy to overlook because disabled states are often handled with opacity or a wrapper &lt;code&gt;div&lt;/code&gt; instead.&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;enabled:hover:&lt;/code&gt; keeps your intent explicit in the markup and makes disabled state handling a one-liner in your component library.&lt;/p&gt;
&lt;h1&gt;In practice (Phoenix / LiveView)&lt;/h1&gt;
&lt;p&gt;If you have a reusable button component, this is the ideal place to apply it. Instead of:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;string&quot;&gt;&quot;hover:bg-gray-300 hover:cursor-pointer&quot;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Write:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;string&quot;&gt;&quot;enabled:hover:bg-gray-300 enabled:hover:cursor-pointer&quot;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now any caller that passes &lt;code&gt;disabled&lt;/code&gt; as an attribute gets correct visual behavior automatically — no special-case classes needed at the call site.&lt;/p&gt;
&lt;p&gt;Small trick, but it saves a prop, a conditional, and a subtle UX bug all at once.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/frontend&quot;&gt;#frontend&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/css&quot;&gt;#css&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-27T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/the-tailwind-enabled-selector-trick-for-disabled-buttons</id>
    <title>🐥 The Tailwind `enabled:` selector trick for disabled buttons</title>
    <updated>2026-05-27T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/hide-a-layout-section-from-a-specific-child-view-in-laravel-blade" rel="alternate"/>
    <content type="html">&lt;p&gt;Sometimes you have a shared layout that renders a block — a promotional banner, a sidebar widget, an image — that makes sense on most pages but not all. Rather than duplicating the layout or reaching for JavaScript, you can solve this cleanly with two built-in Blade directives: &lt;code&gt;@section&lt;/code&gt; and &lt;code&gt;View::hasSection()&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;The problem&lt;/h1&gt;
&lt;p&gt;Say your login layout renders a decorative image at the bottom of every page:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&amp;lbrace;&amp;lbrace;-- resources/views/layouts/login.blade.php --&amp;rbrace;&amp;rbrace;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;@php $loginImage = Contractify::loginImage(); @endphp
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;@if ($loginImage-&gt;src)
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;div class=&quot;...&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;        &lt;img src=&quot;&amp;lbrace;&amp;lbrace; $loginImage-&gt;src &amp;rbrace;&amp;rbrace;&quot; alt=&quot;&amp;lbrace;&amp;lbrace; $loginImage-&gt;alt &amp;rbrace;&amp;rbrace;&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;/div&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;@endif
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is fine for the login and registration pages, but on the OAuth authorization page it&apos;s visual noise you&apos;d rather skip.&lt;/p&gt;
&lt;h1&gt;The solution&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Wrap the block in the layout with &lt;code&gt;@unless(View::hasSection(...))&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&amp;lbrace;&amp;lbrace;-- resources/views/layouts/login.blade.php --&amp;rbrace;&amp;rbrace;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;@unless(View::hasSection(&amp;#39;hide-login-image&amp;#39;))
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    @php $loginImage = Contractify::loginImage(); @endphp
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    @if ($loginImage-&gt;src)
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;        &lt;div class=&quot;...&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;            &lt;img src=&quot;&amp;lbrace;&amp;lbrace; $loginImage-&gt;src &amp;rbrace;&amp;rbrace;&quot; alt=&quot;&amp;lbrace;&amp;lbrace; $loginImage-&gt;alt &amp;rbrace;&amp;rbrace;&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;/div&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    @endif
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;@endunless
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Declare the empty section in the child view that should opt out:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&amp;lbrace;&amp;lbrace;-- resources/views/vendor/passport/authorize.blade.php --&amp;rbrace;&amp;rbrace;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;@extends(&amp;#39;layouts.login&amp;#39;)
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;@section(&amp;#39;hide-login-image&amp;#39;)@endsection
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;@section(&amp;#39;content&amp;#39;)
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &amp;lbrace;&amp;lbrace;-- ... --&amp;rbrace;&amp;rbrace;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;@endsection
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;How it works&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;View::hasSection(&apos;hide-login-image&apos;)&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; if the currently-rendering child view has declared a section with that name — even an empty one. The &lt;code&gt;@unless&lt;/code&gt; then skips the block entirely.&lt;/p&gt;
&lt;p&gt;Every other view that extends the layout leaves that section undeclared, so &lt;code&gt;hasSection&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt; and the image renders as normal.&lt;/p&gt;
&lt;h1&gt;Why this is better than the alternatives&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No layout duplication.&lt;/strong&gt; You keep a single source of truth.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No conditionals based on route names or controller names.&lt;/strong&gt; Those couple your layout to your routing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The opt-out lives in the view that needs it.&lt;/strong&gt; Easy to find, easy to remove.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&apos;s a tiny pattern, but it keeps your layouts clean and your child views in control of their own presentation.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/frontend&quot;&gt;#frontend&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-25T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/hide-a-layout-section-from-a-specific-child-view-in-laravel-blade</id>
    <title>🐥 Hide a layout section from a specific child view in Laravel Blade</title>
    <updated>2026-05-25T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/scanning-files-with-clamdscan-and-fdpass" rel="alternate"/>
    <content type="html">&lt;p&gt;When integrating antivirus scanning into a Linux application or upload pipeline, &lt;code&gt;clamdscan&lt;/code&gt; is usually a much better choice than &lt;code&gt;clamscan&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;While &lt;code&gt;clamscan&lt;/code&gt; starts a full standalone scan process and loads virus signatures every time, &lt;code&gt;clamdscan&lt;/code&gt; talks to the long-running &lt;code&gt;clamd&lt;/code&gt; daemon. This makes scans significantly faster and more suitable for production environments.&lt;/p&gt;
&lt;p&gt;One particularly useful option is &lt;code&gt;--fdpass&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;What does --fdpass do?&lt;/h1&gt;
&lt;p&gt;Normally, &lt;code&gt;clamd&lt;/code&gt; runs under its own user account, such as &lt;code&gt;clamav&lt;/code&gt;. This can create permission issues when scanning files owned by another user or temporary upload files.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;--fdpass&lt;/code&gt; option solves this by passing the already opened file descriptor to the daemon instead of letting the daemon reopen the file itself.&lt;/p&gt;
&lt;p&gt;This is especially useful for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Upload processing&lt;/li&gt;
&lt;li&gt;Temporary files&lt;/li&gt;
&lt;li&gt;Restricted file permissions&lt;/li&gt;
&lt;li&gt;CI/CD pipelines&lt;/li&gt;
&lt;li&gt;Web applications&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Installing ClamAV&lt;/h1&gt;
&lt;p&gt;On Debian or Ubuntu systems:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;update&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clamav&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clamav-daemon&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the virus definitions:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;freshclam&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Enabling clamd on startup&lt;/h1&gt;
&lt;p&gt;To ensure the daemon automatically starts after a reboot:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clamav-daemon&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;start&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clamav-daemon&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify the service is running:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clamav-daemon&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On some distributions, the service may be named &lt;code&gt;clamd&lt;/code&gt; instead.&lt;/p&gt;
&lt;h1&gt;Verifying the socket configuration&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;clamdscan&lt;/code&gt; communicates with the daemon through a Unix socket.&lt;/p&gt;
&lt;p&gt;Check the configured socket path:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;LocalSocket&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/etc/clamav/clamd.conf&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Typical values are:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;LocalSocket /run/clamav/clamd.ctl
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;LocalSocket /var/run/clamav/clamd.ctl
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also locate the socket manually:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;find&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/run&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/var/run&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-name&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;*.ctl&quot;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Scanning a file&lt;/h1&gt;
&lt;p&gt;To scan a file quietly:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;clamdscan&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--fdpass&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--quiet&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;myfile.zip&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the file is clean, the command exits with status code &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If malware is detected, the exit code becomes &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Example in a shell script:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;clamdscan&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--fdpass&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--quiet&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;upload.pdf&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;then&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;    &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;File is clean&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;else&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Virus detected&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;fi&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Why use clamdscan instead of clamscan?&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;clamdscan&lt;/code&gt; has several advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Much faster scanning&lt;/li&gt;
&lt;li&gt;Lower memory usage&lt;/li&gt;
&lt;li&gt;Better suited for concurrent workloads&lt;/li&gt;
&lt;li&gt;No repeated signature loading&lt;/li&gt;
&lt;li&gt;Ideal for web servers and APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For applications handling many uploads or background jobs, using &lt;code&gt;clamdscan --fdpass&lt;/code&gt; is generally the recommended setup.&lt;/p&gt;
&lt;h1&gt;Common error&lt;/h1&gt;
&lt;p&gt;If you encounter:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;ERROR: Could not connect to clamd on LocalSocket
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the daemon is usually not running.&lt;/p&gt;
&lt;p&gt;Start it manually:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;start&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clamav-daemon&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then retry the scan command.&lt;/p&gt;
&lt;h1&gt;Understanding exit codes&lt;/h1&gt;
&lt;p&gt;When integrating &lt;code&gt;clamdscan&lt;/code&gt; into an application or upload pipeline, it is important to distinguish between an actual virus detection and a technical failure.&lt;/p&gt;
&lt;p&gt;Fortunately, &lt;code&gt;clamdscan&lt;/code&gt; provides clear exit codes for this.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exit code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No virus found&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Virus found&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scanner error or operational failure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This means you should avoid parsing stdout or stderr to determine the scan result. Instead, rely on the process exit code directly.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;clamdscan&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--fdpass&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--quiet&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;upload.pdf&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;variable&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;?&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;in&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;File is clean&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;punctuation-delimiter&quot;&gt;;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Virus detected&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;punctuation-delimiter&quot;&gt;;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;        &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Scan failed&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;        &lt;span class=&quot;punctuation-delimiter&quot;&gt;;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;esac&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This distinction is especially important in automated systems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Exit code &lt;code&gt;1&lt;/code&gt; means the scanner worked correctly and detected malware&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Exit code &lt;code&gt;2&lt;/code&gt; means something operational failed, such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;clamd&lt;/code&gt; not running&lt;/li&gt;
&lt;li&gt;socket connection failures&lt;/li&gt;
&lt;li&gt;permission issues&lt;/li&gt;
&lt;li&gt;corrupted scanner configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In production systems, a virus detection is usually a business-level validation failure, while exit code &lt;code&gt;2&lt;/code&gt; should typically trigger retries, logging, monitoring, or alerts.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/devops&quot;&gt;#devops&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/linux&quot;&gt;#linux&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sysadmin&quot;&gt;#sysadmin&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-23T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/scanning-files-with-clamdscan-and-fdpass</id>
    <title>🐥 Scanning files with clamdscan and --fdpass</title>
    <updated>2026-05-23T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/typescripts-pick-utility-type-select-what-you-need" rel="alternate"/>
    <content type="html">&lt;p&gt;TypeScript ships with a set of built-in utility types that make working with existing types far more ergonomic. One of the most useful is &lt;code&gt;Pick&lt;Type, Keys&gt;&lt;/code&gt; — a simple but powerful tool for carving out a subset of properties from a larger type.&lt;/p&gt;
&lt;h1&gt;What Is &lt;code&gt;Pick&lt;/code&gt;?&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;Pick&lt;Type, Keys&gt;&lt;/code&gt; constructs a new type by selecting a specific set of properties (&lt;code&gt;Keys&lt;/code&gt;) from an existing type (&lt;code&gt;Type&lt;/code&gt;). Think of it as a projection: you have a big object shape, and you only want a slice of it.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Pick&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;K&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;keyof&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;P&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;K&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;P&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;A Practical Example&lt;/h1&gt;
&lt;p&gt;Suppose you have a &lt;code&gt;User&lt;/code&gt; type across your application:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;passwordHash&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;createdAt&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;admin&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;editor&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;viewer&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When building a public-facing API response, you never want to expose &lt;code&gt;passwordHash&lt;/code&gt;. And a user profile card in the UI might only care about &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, and &lt;code&gt;email&lt;/code&gt;. Instead of duplicating the shape or manually re-declaring those fields, reach for &lt;code&gt;Pick&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;PublicUser&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Pick&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;id&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;email&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// Equivalent to:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// type PublicUser = &amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;comment&quot;&gt;//   id: number;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;comment&quot;&gt;//   name: string;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;comment&quot;&gt;//   email: string;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// &amp;rbrace;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now &lt;code&gt;PublicUser&lt;/code&gt; stays in sync with &lt;code&gt;User&lt;/code&gt; automatically. If you rename &lt;code&gt;name&lt;/code&gt; to &lt;code&gt;fullName&lt;/code&gt; on &lt;code&gt;User&lt;/code&gt;, TypeScript will immediately flag any &lt;code&gt;Pick&lt;/code&gt; references to &lt;code&gt;&apos;name&apos;&lt;/code&gt; as an error.&lt;/p&gt;
&lt;h1&gt;Real-World Use Cases&lt;/h1&gt;
&lt;h2&gt;Form State&lt;/h2&gt;
&lt;p&gt;When a form only edits a subset of a model&apos;s fields, &lt;code&gt;Pick&lt;/code&gt; keeps the shape accurate without duplication:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;UpdateProfileForm&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Pick&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;email&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;submitProfileUpdate&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;UpdateProfileForm&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;api&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;/profile&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Component Props&lt;/h2&gt;
&lt;p&gt;Vue and React components often need just a few fields from a larger model. &lt;code&gt;Pick&lt;/code&gt; makes this explicit in the prop type:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;AvatarProps&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Pick&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;id&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;sm&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;md&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;lg&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Service Layer Boundaries&lt;/h2&gt;
&lt;p&gt;When passing data between services, &lt;code&gt;Pick&lt;/code&gt; enforces that only the relevant fields cross the boundary — a lightweight form of data encapsulation:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;EmailPayload&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Pick&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;email&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;sendWelcomeEmail&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;EmailPayload&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;mailer&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;subject&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;`Welcome, &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;!`&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Combining with Other Utility Types&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Pick&lt;/code&gt; composes cleanly with other utilities. For example, &lt;code&gt;Partial&lt;Pick&lt;...&gt;&gt;&lt;/code&gt; gives you an optional subset — perfect for PATCH request bodies:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;PatchUserRequest&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Partial&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Pick&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;email&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;role&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;&lt;code&gt;Pick&lt;/code&gt; vs &lt;code&gt;Omit&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;Pick&lt;/code&gt; and &lt;code&gt;Omit&lt;/code&gt; are mirror images of each other:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Utility&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Best when…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Pick&lt;Type, Keys&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allowlist — name what you want&lt;/td&gt;
&lt;td&gt;The desired set is small&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Omit&lt;Type, Keys&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Denylist — name what you don&apos;t want&lt;/td&gt;
&lt;td&gt;The excluded set is small&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you want all fields except one or two, &lt;code&gt;Omit&lt;/code&gt; is more concise. If you want just a handful of fields from a large type, &lt;code&gt;Pick&lt;/code&gt; is cleaner.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// These are equivalent when User has exactly these five fields:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;WithoutPassword&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Omit&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;passwordHash&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;SafeFields&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Pick&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;id&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;email&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;createdAt&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;role&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// Prefer Omit here — fewer keys to list&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Key Takeaways&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Pick&lt;Type, Keys&gt;&lt;/code&gt; creates a new type with only the named properties from &lt;code&gt;Type&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Keys&lt;/code&gt; argument is validated against &lt;code&gt;keyof Type&lt;/code&gt; at compile time — typos are caught immediately.&lt;/li&gt;
&lt;li&gt;It keeps derived types in sync with their source; renames and removals surface as errors rather than silent mismatches.&lt;/li&gt;
&lt;li&gt;It composes well with &lt;code&gt;Partial&lt;/code&gt;, &lt;code&gt;Required&lt;/code&gt;, &lt;code&gt;Readonly&lt;/code&gt;, and &lt;code&gt;Omit&lt;/code&gt; for expressive, minimal type definitions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Whenever you find yourself re-declaring a handful of fields that already exist on another type, &lt;code&gt;Pick&lt;/code&gt; is almost certainly the right tool.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/typescript&quot;&gt;#typescript&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/frontend&quot;&gt;#frontend&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-21T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/typescripts-pick-utility-type-select-what-you-need</id>
    <title>🐥 TypeScript&apos;s Pick utility type: select what you need</title>
    <updated>2026-05-21T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/color-code-your-github-pr-list-with-custom-css" rel="alternate"/>
    <content type="html">&lt;p&gt;If you spend a lot of time reviewing pull requests on GitHub, you&apos;ve probably wished the PR list gave you more visual signal at a glance. Which PRs are approved and ready to merge? Which ones have a reviewer blocking them? Which are still drafts?&lt;/p&gt;
&lt;p&gt;With the &lt;a href=&quot;https://github.com/refined-github/refined-github&quot;&gt;Refined GitHub&lt;/a&gt; browser extension and a few lines of custom CSS, you can color-code your PR list to answer all of those questions instantly.&lt;/p&gt;
&lt;h1&gt;What You&apos;ll Need&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/refined-github/refined-github&quot;&gt;Refined GitHub&lt;/a&gt; — a browser extension for Chrome, Firefox, and Safari that enhances the GitHub UI with dozens of quality-of-life improvements. One of its lesser-known features is the ability to inject your own custom CSS.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Setting Up Custom CSS in Refined GitHub&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;Install Refined GitHub from your browser&apos;s extension store.&lt;/li&gt;
&lt;li&gt;Click the Refined GitHub icon in your toolbar and open its &lt;strong&gt;Options&lt;/strong&gt; page.&lt;/li&gt;
&lt;li&gt;Scroll down to the &lt;strong&gt;Custom CSS&lt;/strong&gt; section.&lt;/li&gt;
&lt;li&gt;Paste the CSS below and save.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;The CSS&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-css&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;/* Draft PRs — targets the draft octicon on the PR status icon */&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;js-issue-row&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;svg&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;octicon-git-pull-request-draft&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;property&quot;&gt;border-right&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;px&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; solid &lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;e68934&lt;/span&gt; &lt;span class=&quot;keyword-modifier&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;230&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;137&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;52&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;number-float&quot;&gt;0.1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;comment&quot;&gt;/* At least one approving review */&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;js-issue-row&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;tag-attribute&quot;&gt;aria-label&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;*=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;review approval&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;property&quot;&gt;border-right&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;px&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; solid &lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;5eb4a7&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;94&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;180&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;167&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number-float&quot;&gt;0.2&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;&lt;span class=&quot;comment&quot;&gt;/* Changes Requested */&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;js-issue-row&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;tag-attribute&quot;&gt;aria-label&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;*=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;review requesting changes&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;js-issue-row&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;tag-attribute&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;Changes requested&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;property&quot;&gt;border-right&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;px&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; solid &lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;9b59b6&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;    &lt;span class=&quot;property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;155&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;89&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;182&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;number-float&quot;&gt;0.1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword-modifier&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;How It Works&lt;/h1&gt;
&lt;p&gt;GitHub&apos;s PR list uses SVG status icons and review-state badges to indicate a PR&apos;s current state. The CSS uses the &lt;code&gt;:has()&lt;/code&gt; selector to look up from those indicators to the parent row and apply a colored right border and tinted background.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Selector&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Color&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;svg.octicon-git-pull-request-draft&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Draft PR&lt;/td&gt;
&lt;td&gt;Orange &lt;code&gt;#e68934&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[aria-label*=&quot;review approval&quot;]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Approved&lt;/td&gt;
&lt;td&gt;Teal &lt;code&gt;#5eb4a7&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[aria-label*=&quot;review requesting changes&quot;]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Changes requested&lt;/td&gt;
&lt;td&gt;Purple &lt;code&gt;#9b59b6&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;The Result&lt;/h1&gt;
&lt;p&gt;Head to any GitHub PR list and you&apos;ll immediately see rows highlighted by status. Approved PRs glow teal, blocked ones turn purple, and drafts stand out in orange — no more scanning each row individually.&lt;/p&gt;
&lt;p&gt;It&apos;s a small change, but when you&apos;re triaging dozens of open PRs every day, the visual grouping saves a surprising amount of time.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/css&quot;&gt;#css&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/github&quot;&gt;#github&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-19T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/color-code-your-github-pr-list-with-custom-css</id>
    <title>🐥 Color-code your GitHub PR list with custom CSS</title>
    <updated>2026-05-19T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/why-dexter-lsp-doesnt-autocomplete-elixir-stdlib-modules" rel="alternate"/>
    <content type="html">&lt;p&gt;If you&apos;re using &lt;a href=&quot;https://github.com/elixir-tools/dexter&quot;&gt;Dexter&lt;/a&gt; as your Elixir LSP in VS Code and wondering why &lt;code&gt;String&lt;/code&gt;, &lt;code&gt;Enum&lt;/code&gt;, and other standard library modules don&apos;t show up in autocomplete — the answer is likely a missing or misconfigured &lt;code&gt;stdlibPath&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;The Problem&lt;/h1&gt;
&lt;p&gt;Dexter needs access to the &lt;strong&gt;Elixir source files&lt;/strong&gt; (&lt;code&gt;.ex&lt;/code&gt;) to index the stdlib. When you install Elixir via Homebrew, only compiled &lt;code&gt;.beam&lt;/code&gt; files are included — no source. So even if you point &lt;code&gt;dexter.stdlibPath&lt;/code&gt; at something like:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-json&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;text&quot;&gt;&quot;dexter.stdlibPath&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; &quot;/opt/homebrew/opt/elixir/lib/elixir/lib&quot;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...there&apos;s nothing for Dexter to index, and stdlib autocomplete silently doesn&apos;t work.&lt;/p&gt;
&lt;h1&gt;The Fix&lt;/h1&gt;
&lt;p&gt;Clone the Elixir source at the version matching your runtime:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;elixir&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--version&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clone&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;https://github.com/elixir-lang/elixir.git&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;~/elixir-src&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--depth=1&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--branch&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;v1.18.3&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then update your &lt;code&gt;.vscode/settings.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-json&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;text&quot;&gt;&quot;dexter.stdlibPath&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; &quot;/Users/yourname/elixir-src/lib&quot;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;lib/&lt;/code&gt; directory contains subdirectories like &lt;code&gt;elixir/lib/&lt;/code&gt;, &lt;code&gt;mix/lib/&lt;/code&gt;, &lt;code&gt;logger/lib/&lt;/code&gt;, etc. — all with proper &lt;code&gt;.ex&lt;/code&gt; source files that Dexter can index for autocomplete.&lt;/p&gt;
&lt;p&gt;After restarting VS Code, stdlib completions for &lt;code&gt;String&lt;/code&gt;, &lt;code&gt;Enum&lt;/code&gt;, &lt;code&gt;Map&lt;/code&gt;, and friends should start appearing.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/vscode&quot;&gt;#vscode&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-17T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/why-dexter-lsp-doesnt-autocomplete-elixir-stdlib-modules</id>
    <title>🐥 Why Dexter LSP doesn&apos;t autocomplete Elixir stdlib modules</title>
    <updated>2026-05-17T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/keeping-elixir-stdlib-source-in-sync-with-your-project" rel="alternate"/>
    <content type="html">&lt;p&gt;When working with tools like &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=dexter.dexter&quot;&gt;Dexter&lt;/a&gt; for Elixir code intelligence in VS Code, it&apos;s useful to have the Elixir standard library source checked out locally. Dexter uses it to provide go-to-definition and hover docs for stdlib modules.&lt;/p&gt;
&lt;p&gt;The naive approach — cloning it once by hand — breaks down the moment you upgrade Elixir. You forget to re-clone, the path is wrong, and suddenly your editor is jumping to source that doesn&apos;t match what&apos;s actually running.&lt;/p&gt;
&lt;p&gt;Here&apos;s a small shell script that automates it.&lt;/p&gt;
&lt;h1&gt;The script&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-directive&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-builtin&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-euo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;pipefail&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;variable&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;elixir&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--version&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-oE&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;Elixir [0-9]+\.[0-9]+\.[0-9]+&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;-1&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;&amp;lbrace;print $2&amp;rbrace;&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;then&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Error: could not determine Elixir version&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;function-builtin&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;fi&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;variable&quot;&gt;branch&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;v&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;&lt;span class=&quot;variable&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;function-builtin&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;dirname&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;function-builtin&quot;&gt;pwd&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;/.elixir_stdlib&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;&lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Elixir version: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;then&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;  &lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-C&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;tag&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--points-at&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;HEAD&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;string-special-path&quot;&gt;/dev/null&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-qx&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;branch&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;then&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;    &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt; already at &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;branch&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;, skipping.&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;    &lt;span class=&quot;function-builtin&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;  &lt;span class=&quot;keyword-conditional&quot;&gt;fi&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;  &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Found &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt; at a different version, removing for &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;branch&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt; ...&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-rf&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;fi&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;&lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Cloning branch &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;branch&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt; into &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt; ...&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;advice.detachedHead=false&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clone&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;https://github.com/elixir-lang/elixir.git&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--depth=1&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--branch&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;branch&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;What it does&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Detects the running Elixir version.&lt;/strong&gt; It calls &lt;code&gt;elixir --version&lt;/code&gt; and extracts the version number, so there&apos;s no hardcoded version to keep in sync manually.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Clones into &lt;code&gt;.elixir_stdlib&lt;/code&gt; next to the script.&lt;/strong&gt; Using &lt;code&gt;dirname &quot;$0&quot;&lt;/code&gt; makes the destination relative to the script itself, so it works regardless of your current working directory.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Skips the clone if already up to date.&lt;/strong&gt; Before doing anything destructive, it checks whether the target tag (&lt;code&gt;v1.19.5&lt;/code&gt;, for example) is among the tags pointing at &lt;code&gt;HEAD&lt;/code&gt; in the existing clone. This matters because the Elixir repo uses floating tags like &lt;code&gt;v1.19-latest&lt;/code&gt; that point at the same commit — a naive &lt;code&gt;git describe&lt;/code&gt; would return that tag instead, causing a false version mismatch every time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Wipes and re-clones on version change.&lt;/strong&gt; If the installed Elixir version doesn&apos;t match what&apos;s checked out, the old folder is removed and the correct version is cloned fresh.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Suppresses the detached HEAD warning.&lt;/strong&gt; Cloning a tag always results in a detached HEAD, which git warns about by default. Passing &lt;code&gt;-c advice.detachedHead=false&lt;/code&gt; silences that noise without hiding anything useful.&lt;/p&gt;
&lt;h1&gt;Usage&lt;/h1&gt;
&lt;p&gt;Drop the script in your project root, make it executable, add &lt;code&gt;.elixir_stdlib&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;, and point your editor at &lt;code&gt;$&amp;lbrace;workspaceFolder&amp;rbrace;/.elixir_stdlib/lib&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;chmod&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;+x&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clone-elixir-src.sh&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;./clone-elixir-src.sh&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it again whenever you upgrade Elixir — it&apos;ll figure out the rest.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/terminal&quot;&gt;#terminal&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/vscode&quot;&gt;#vscode&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-15T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/keeping-elixir-stdlib-source-in-sync-with-your-project</id>
    <title>🐥 Keeping Elixir stdlib source in sync with your project</title>
    <updated>2026-05-15T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/anti-corruption-layer-in-elixir-phoenix-keep-your-domain-clean" rel="alternate"/>
    <content type="html">&lt;p&gt;When you integrate an external service — a payment provider, a shipping API, a CRM — you face a quiet but persistent risk: the external system&apos;s model starts leaking into your own. Status strings, provider-specific IDs, and SDK structs end up scattered across your business logic. Changing providers later means touching code you should never have had to change.&lt;/p&gt;
&lt;p&gt;The Anti-Corruption Layer (ACL) is a pattern from Domain-Driven Design that puts an explicit translation boundary between your domain and the external world. Everything provider-specific lives in one place. Your domain never knows it exists.&lt;/p&gt;
&lt;p&gt;This post walks through implementing it in Elixir and Phoenix, using a payment integration as the running example.&lt;/p&gt;
&lt;h1&gt;The Problem&lt;/h1&gt;
&lt;p&gt;Suppose you&apos;re integrating Stripe directly. Without a boundary, it&apos;s easy to write something like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Orders&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;complete_order&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Req&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;https://api.stripe.com/v1/payment_intents&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;           &lt;span class=&quot;string-special-symbol&quot;&gt;auth: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:basic&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;stripe_key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;           &lt;span class=&quot;string-special-symbol&quot;&gt;form: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;amount: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;total_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;currency: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;eur&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;confirm: &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;         &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;status: &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;body: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;status&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;succeeded&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;module&quot;&gt;Orders&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;mark_paid&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;body: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;error&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;message&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works on day one. But now your &lt;code&gt;Orders&lt;/code&gt; context knows that payments have a &lt;code&gt;&quot;succeeded&quot;&lt;/code&gt; status string, that IDs come from &lt;code&gt;intent[&quot;id&quot;]&lt;/code&gt;, and that Stripe&apos;s API lives at that specific URL. If you ever swap providers — or just want to test without hitting Stripe — you have to unpick all of this from business logic.&lt;/p&gt;
&lt;h1&gt;Core Concepts in Elixir&lt;/h1&gt;
&lt;p&gt;The ACL pattern maps cleanly to Elixir idioms:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;DDD concept&lt;/th&gt;
&lt;th&gt;Elixir equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Domain model&lt;/td&gt;
&lt;td&gt;Plain &lt;code&gt;defstruct&lt;/code&gt; (not an Ecto schema)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Port / interface&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;@behaviour&lt;/code&gt; module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACL implementation&lt;/td&gt;
&lt;td&gt;A module implementing the behaviour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain service&lt;/td&gt;
&lt;td&gt;A context module that only calls the behaviour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wiring&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Application.get_env/2&lt;/code&gt; and config files&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;Step 1: Define Your Domain Structs&lt;/h1&gt;
&lt;p&gt;These live in your domain and use your language — not Stripe&apos;s.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.PaymentResult&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;type &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:pending&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:captured&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:declined&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:refunded&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;enforce_keys &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:status&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:currency&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defstruct&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:status&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:currency&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.PaymentEvent&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;type &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:payment_captured&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:payment_declined&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:payment_refunded&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;enforce_keys &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:type&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:payment_id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defstruct&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:type&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:payment_id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No Stripe types, no raw maps, no magic strings. If you switch providers, these structs stay exactly as they are.&lt;/p&gt;
&lt;h1&gt;Step 2: Define the Port Behaviour&lt;/h1&gt;
&lt;p&gt;The behaviour declares what the domain needs — a contract, not an implementation.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.PaymentGateway&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;type &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;charge_result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.PaymentResult&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;type &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;refund_result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.PaymentResult&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;callback &lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;charge&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount_cents&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;integer&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;currency&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;              &lt;span class=&quot;function-call&quot;&gt;charge_result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;callback &lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;payment_id&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;refund_result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The domain context will call through this behaviour. It never imports Stripe modules or touches raw HTTP responses.&lt;/p&gt;
&lt;h1&gt;Step 3: Implement the ACL&lt;/h1&gt;
&lt;p&gt;This is the only module allowed to know anything about Stripe. All HTTP calls and all translation logic live here.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.Gateways.StripeGateway&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;behaviour &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentGateway&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;base_url &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;https://api.stripe.com/v1&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;impl &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;charge&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;currency&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;      &lt;span class=&quot;module&quot;&gt;Req&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;post!&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;#&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;base_url&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;/payment_intents&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;string-special-symbol&quot;&gt;auth: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:basic&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;secret_key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;string-special-symbol&quot;&gt;form: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;          &lt;span class=&quot;string-special-symbol&quot;&gt;amount: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;          &lt;span class=&quot;string-special-symbol&quot;&gt;currency: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;currency&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;          &lt;span class=&quot;string-special-symbol&quot;&gt;payment_method: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;          &lt;span class=&quot;string-special-symbol&quot;&gt;confirm: &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;          &lt;span class=&quot;string-special-symbol&quot;&gt;&quot;automatic_payment_methods[enabled]&quot;: &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;          &lt;span class=&quot;string-special-symbol&quot;&gt;&quot;automatic_payment_methods[allow_redirects]&quot;: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;never&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;      &lt;span class=&quot;number&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;translate_intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;      &lt;span class=&quot;comment&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;impl &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;28&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;payment_id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;29&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;30&quot;&gt;      &lt;span class=&quot;module&quot;&gt;Req&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;post!&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;#&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;base_url&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;/refunds&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;31&quot;&gt;        &lt;span class=&quot;string-special-symbol&quot;&gt;auth: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:basic&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;secret_key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;32&quot;&gt;        &lt;span class=&quot;string-special-symbol&quot;&gt;form: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;payment_intent: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;payment_id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;33&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;34&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;35&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;36&quot;&gt;      &lt;span class=&quot;number&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;translate_refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;37&quot;&gt;      &lt;span class=&quot;comment&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;38&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;39&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;40&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;41&quot;&gt;  &lt;span class=&quot;comment&quot;&gt;# --- Translation layer ---&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;42&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;43&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;44&quot;&gt;    &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentResult&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;45&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;46&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;status: &lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;translate_status&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;status&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;47&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;amount_cents: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;48&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;currency: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;currency&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;49&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;50&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;51&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;52&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;53&quot;&gt;    &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentResult&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;54&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;payment_intent&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;55&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;status: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:refunded&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;56&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;amount_cents: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;57&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;currency: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;currency&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;58&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;59&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;60&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;61&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_status&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;succeeded&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:captured&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;62&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_status&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;processing&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:pending&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;63&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_status&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;requires_payment_method&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:declined&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;64&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_status&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;canceled&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:declined&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;65&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_status&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;comment&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:pending&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;66&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;67&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;secret_key&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;fetch_env!&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:my_app&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:stripe_secret_key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;68&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pattern matching on &lt;code&gt;translate_status/1&lt;/code&gt; makes the translation explicit and exhaustive. Adding a new Stripe status is a one-line change in exactly one place.&lt;/p&gt;
&lt;h1&gt;Step 4: Write a Clean Domain Context&lt;/h1&gt;
&lt;p&gt;The &lt;code&gt;Billing&lt;/code&gt; context only speaks the behaviour. It has no idea which gateway is wired up.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;process_payment&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;currency&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;gateway&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;charge&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;currency&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;issue_refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;payment_id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;gateway&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;payment_id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;gateway&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;module&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get_env&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:my_app&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:payment_gateway&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.Gateways.StripeGateway&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing in here ties you to Stripe. The context is pure business logic.&lt;/p&gt;
&lt;h1&gt;Step 5: Wire It Up in Config&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# config/config.exs&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:my_app&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:payment_gateway&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.Gateways.StripeGateway&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:my_app&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:stripe_secret_key&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;System&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get_env&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;STRIPE_SECRET_KEY&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# config/test.exs&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:my_app&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:payment_gateway&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.Gateways.FakeGateway&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Switching providers in production means changing one config line and shipping a new gateway module. Nothing else changes.&lt;/p&gt;
&lt;h1&gt;Handling Webhooks&lt;/h1&gt;
&lt;p&gt;Webhooks are where external models are most tempting to let through. A Phoenix controller should translate immediately and dispatch a domain event — it should not pass raw Stripe payloads any further.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyAppWeb.StripeWebhookController&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyAppWeb&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:controller&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;translate_event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;module&quot;&gt;Billing&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;handle_payment_event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;function-call&quot;&gt;send_resp&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:unhandled&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;function-call&quot;&gt;send_resp&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;reason&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;        &lt;span class=&quot;function-call&quot;&gt;send_resp&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;reason&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;         &lt;span class=&quot;string&quot;&gt;&quot;type&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;payment_intent.succeeded&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;         &lt;span class=&quot;string&quot;&gt;&quot;data&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;object&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;id&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;amount&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;amount&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;       &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentEvent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;type: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:payment_captured&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;payment_id: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;amount_cents: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;         &lt;span class=&quot;string&quot;&gt;&quot;type&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;payment_intent.payment_failed&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;         &lt;span class=&quot;string&quot;&gt;&quot;data&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;object&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;id&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;amount&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;amount&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;28&quot;&gt;       &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;29&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentEvent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;type: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:payment_declined&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;payment_id: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;amount_cents: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;30&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;31&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;32&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;comment&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:unhandled&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;33&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The controller is the ACL for inbound Stripe events. Once translated, &lt;code&gt;Billing.handle_payment_event/1&lt;/code&gt; receives only &lt;code&gt;%Billing.PaymentEvent&amp;lbrace;&amp;rbrace;&lt;/code&gt; structs — never raw Stripe data.&lt;/p&gt;
&lt;h1&gt;Testing Without a Real Gateway&lt;/h1&gt;
&lt;p&gt;Because the gateway is a behaviour, you can swap in a fake implementation for tests without mocks or HTTP stubs.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.Gateways.FakeGateway&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;behaviour &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentGateway&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;impl &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;charge&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;comment&quot;&gt;_amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_currency&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;fail_card&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Card declined&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;charge&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;currency&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_source&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentResult&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;fake_&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;#&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;System&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;unique_integer&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:positive&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;status: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:captured&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;amount_cents: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;currency: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;currency&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;impl &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;payment_id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentResult&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;payment_id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;status: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:refunded&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;amount_cents: &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;currency: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;eur&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your tests call &lt;code&gt;Billing.process_payment/3&lt;/code&gt; exactly as production code does — there is no test-only branch in the domain logic, and no HTTP traffic.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;successful payment returns a captured result&quot;&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentResult&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;status: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:captured&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;           &lt;span class=&quot;module&quot;&gt;Billing&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;process_payment&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;1999&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;eur&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;tok_visa&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;declined card returns an error&quot;&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Card declined&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;           &lt;span class=&quot;module&quot;&gt;Billing&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;process_payment&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;1999&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;eur&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;fail_card&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Elixir-Specific Considerations&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Behaviours vs. protocols.&lt;/strong&gt; Use a &lt;code&gt;@behaviour&lt;/code&gt; when you have multiple implementations of the same concept (payment gateways, SMS providers). Use a &lt;code&gt;protocol&lt;/code&gt; when you want polymorphism across data types you don&apos;t control. For the ACL pattern, behaviours are the right tool.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pattern matching is your translation engine.&lt;/strong&gt; The &lt;code&gt;translate_status/1&lt;/code&gt; clauses above are cleaner than a &lt;code&gt;case&lt;/code&gt; block or a map lookup. If Stripe adds a new status you haven&apos;t handled, the &lt;code&gt;_&lt;/code&gt; clause captures it safely, and you can add a specific clause when you care about it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep Ecto schemas out of the ACL.&lt;/strong&gt; It&apos;s tempting to map directly from a Stripe response onto an Ecto schema. Resist this. Plain structs are easier to reason about, test, and evolve independently from your database shape. Persist only after your domain logic has run.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stateful gateways.&lt;/strong&gt; If your gateway implementation needs its own process — say, to maintain an authenticated session or a connection pool — start it in your application&apos;s supervision tree. The behaviour contract stays the same; the implementation just has a &lt;code&gt;start_link/1&lt;/code&gt; that gets wired into the supervisor.&lt;/p&gt;
&lt;h1&gt;Common Pitfalls&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Returning raw maps from the gateway.&lt;/strong&gt; If &lt;code&gt;charge/3&lt;/code&gt; returns &lt;code&gt;%&amp;lbrace;&quot;id&quot; =&gt; ..., &quot;status&quot; =&gt; &quot;succeeded&quot;&amp;rbrace;&lt;/code&gt;, you&apos;ve moved the contamination rather than removed it. Always return your own structs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Putting translation in the Phoenix controller for everything.&lt;/strong&gt; The controller is the right place for webhook translation (it&apos;s the entry point). But if you&apos;re doing translation inside action handlers for outgoing calls, extract it into the gateway module where it belongs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One ACL module for all external systems.&lt;/strong&gt; If you add a shipping provider, create &lt;code&gt;Shipping.Gateways.DHLGateway&lt;/code&gt; with its own behaviour. Don&apos;t grow &lt;code&gt;StripeGateway&lt;/code&gt; into a general-purpose external-systems module.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Letting provider IDs become first-class domain concepts.&lt;/strong&gt; Storing a Stripe payment intent ID as &lt;code&gt;billing_stripe_id&lt;/code&gt; in your domain is a smell — the domain now knows about Stripe. Store it as &lt;code&gt;external_payment_id&lt;/code&gt; or in a separate gateway-specific record that your ACL manages.&lt;/p&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Elixir&apos;s behaviours give you a clean, compiler-checked way to define the boundary between your domain and the outside world. Pattern matching makes the translation logic readable and exhaustive. Swapping configs gives you test isolation without mocks.&lt;/p&gt;
&lt;p&gt;The ACL pattern keeps your core business logic stable regardless of what external APIs do. When Stripe changes a status code or you decide to try a different provider, you update one module. Your domain, your tests, and everything that depends on them stays untouched.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-13T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/anti-corruption-layer-in-elixir-phoenix-keep-your-domain-clean</id>
    <title>🐥 Anti-Corruption Layer in Elixir/Phoenix - Keep your domain clean</title>
    <updated>2026-05-13T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/restarting-supervisord-daemons-by-working-directory-on-linux" rel="alternate"/>
    <content type="html">&lt;p&gt;When you run multiple instances of the same Laravel Horizon worker (or any supervisord-managed process) across different deployment directories, you&apos;ll quickly notice that supervisord assigns generic auto-generated names like &lt;code&gt;daemon-282661:daemon-282661_00&lt;/code&gt;. There&apos;s no obvious way to tell which daemon belongs to which application directory — until you know the trick.&lt;/p&gt;
&lt;h1&gt;Finding the working directory of a process&lt;/h1&gt;
&lt;p&gt;On Linux, every running process exposes its current working directory as a symlink under &lt;code&gt;/proc&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;readlink&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/proc/&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;string-special-path&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;string-special-path&quot;&gt;/cwd&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To check all your Horizon workers at once, grab their PIDs from &lt;code&gt;supervisorctl status&lt;/code&gt; and loop over them:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-repeat&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;91286&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;91287&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;91288&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;91290&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-repeat&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;readlink&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/proc/&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;/cwd&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-repeat&quot;&gt;done&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Restarting daemons for a specific directory&lt;/h1&gt;
&lt;p&gt;Once you can map PIDs to directories, it&apos;s straightforward to build a script that restarts only the daemons running from a given path:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-directive&quot;&gt;#!/bin/bash&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;constant&quot;&gt;TARGET_DIR&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;:?&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;Usage:&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;string&quot;&gt; &lt;working-directory&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;constant&quot;&gt;TARGET_DIR&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;realpath&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;TARGET_DIR&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;supervisorctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;keyword-repeat&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;function-builtin&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-repeat&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-q&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;RUNNING&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;function-builtin&quot;&gt;continue&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;&amp;lbrace;print $1&amp;rbrace;&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-oP&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;pid \K[0-9]+&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;]]&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;function-builtin&quot;&gt;continue&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;cwd&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;readlink&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;/proc/&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;/cwd&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;string-special-path&quot;&gt;/dev/null&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;cwd&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;TARGET_DIR&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;then&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;        &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Restarting &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;string&quot;&gt; (pid &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;, dir &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;cwd&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;)&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;        &lt;span class=&quot;function-call&quot;&gt;supervisorctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;restart&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;    &lt;span class=&quot;keyword-conditional&quot;&gt;fi&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;&lt;span class=&quot;keyword-repeat&quot;&gt;done&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;How it works&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;supervisorctl status&lt;/code&gt; lists all daemons with their current PIDs.&lt;/li&gt;
&lt;li&gt;Non-&lt;code&gt;RUNNING&lt;/code&gt; daemons are skipped (they have no PID to look up).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/proc/&lt;pid&gt;/cwd&lt;/code&gt; resolves the actual working directory for each process.&lt;/li&gt;
&lt;li&gt;Any daemon whose working directory matches the target path (or is a subpath of the target path) gets restarted.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Usage&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;chmod&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;+x&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;restart-daemons.sh&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;./restart-daemons.sh&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/home/forge/my-app/current&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is especially useful on servers running multiple deployments of the same application — for example, a staging environment with both a &lt;code&gt;current&lt;/code&gt; and a &lt;code&gt;previous&lt;/code&gt; release still running workers.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/devops&quot;&gt;#devops&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/terminal&quot;&gt;#terminal&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/linux&quot;&gt;#linux&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sysadmin&quot;&gt;#sysadmin&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-11T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/restarting-supervisord-daemons-by-working-directory-on-linux</id>
    <title>🐥 Restarting supervisord daemons by working directory on Linux</title>
    <updated>2026-05-11T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/make-your-shell-scripts-environment-variables-overridable" rel="alternate"/>
    <content type="html">&lt;p&gt;When writing shell scripts, hardcoded values are a common source of friction. A script that works perfectly on your machine may need adjustments on a colleague&apos;s setup, in CI, or in production — and forcing people to edit the script itself is messy.&lt;/p&gt;
&lt;p&gt;There&apos;s a simple Bash idiom that solves this cleanly.&lt;/p&gt;
&lt;h1&gt;The Problem&lt;/h1&gt;
&lt;p&gt;Consider a typical startup script:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;8888&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;0.0.0.0&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;uvicorn&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;myapp:app&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--host&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--port&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The port is hardcoded. If you need to run two instances, or if &lt;code&gt;8888&lt;/code&gt; is already taken, you have to edit the file.&lt;/p&gt;
&lt;h1&gt;The Fix: &lt;code&gt;$&amp;lbrace;VAR:-default&amp;rbrace;&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;Replace the hardcoded assignment with a default-value expansion:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;:-&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;8888&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;:-&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;0.0.0.0&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;uvicorn&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;myapp:app&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--host&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--port&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The syntax &lt;code&gt;$&amp;lbrace;VAR:-default&amp;rbrace;&lt;/code&gt; means:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Use the value of &lt;code&gt;$VAR&lt;/code&gt; if it is set and non-empty; otherwise use &lt;code&gt;default&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The script&apos;s behaviour is unchanged when no environment variable is provided — the default kicks in. But now callers can override it without touching the file:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;9000&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;./bin/start.sh&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or by exporting it beforehand:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;9000&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;./bin/start.sh&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Why This Matters&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No edits required.&lt;/strong&gt; The script stays clean; overrides happen at call time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI-friendly.&lt;/strong&gt; Most CI systems let you set environment variables per job. Your script picks them up automatically.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Self-documenting.&lt;/strong&gt; The default value lives right next to the variable name, so readers immediately know what to expect.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composable.&lt;/strong&gt; You can layer overrides: a &lt;code&gt;.env&lt;/code&gt; file, a CI variable, or a one-liner prefix — whatever fits the context.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Variants Worth Knowing&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Syntax&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&amp;lbrace;VAR:-default&amp;rbrace;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;default&lt;/code&gt; if &lt;code&gt;VAR&lt;/code&gt; is unset &lt;strong&gt;or empty&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&amp;lbrace;VAR-default&amp;rbrace;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;default&lt;/code&gt; only if &lt;code&gt;VAR&lt;/code&gt; is unset (empty string is kept)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&amp;lbrace;VAR:=default&amp;rbrace;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;default&lt;/code&gt; and &lt;strong&gt;assign it back&lt;/strong&gt; to &lt;code&gt;VAR&lt;/code&gt; if unset or empty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&amp;lbrace;VAR:?message&amp;rbrace;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Abort with &lt;code&gt;message&lt;/code&gt; if &lt;code&gt;VAR&lt;/code&gt; is unset or empty&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For most cases, &lt;code&gt;$&amp;lbrace;VAR:-default&amp;rbrace;&lt;/code&gt; is the right choice. The &lt;code&gt;:=&lt;/code&gt; variant is useful when you want the variable available for the rest of the script without repeating the default.&lt;/p&gt;
&lt;h1&gt;A Real-World Example&lt;/h1&gt;
&lt;p&gt;Here&apos;s a before/after for a uvicorn startup script:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;0.0.0.0&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;8888&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;uvicorn&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;docai.api.app:app&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--host&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--port&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;:-&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;0.0.0.0&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;:-&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;8888&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;uvicorn&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;docai.api.app:app&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--host&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--port&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two characters added per line (&lt;code&gt;$&amp;lbrace;&lt;/code&gt; and &lt;code&gt;:-&lt;/code&gt;), zero behaviour change by default, and now fully overridable from the outside. A small habit with outsized payoff.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/devops&quot;&gt;#devops&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/terminal&quot;&gt;#terminal&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-09T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/make-your-shell-scripts-environment-variables-overridable</id>
    <title>🐥 Make your shell scripts&apos; environment variables overridable</title>
    <updated>2026-05-09T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/using-generators-for-phpunit-data-providers" rel="alternate"/>
    <content type="html">&lt;p&gt;When writing PHPUnit tests, data providers are one of the simplest ways to increase coverage without duplicating test logic. Most examples use arrays, but PHP generators (&lt;code&gt;yield&lt;/code&gt;) are often a better fit: they’re more expressive, memory-efficient, and easier to extend.&lt;/p&gt;
&lt;p&gt;This post walks through a clean, generic approach to using generators for PHPUnit data providers.&lt;/p&gt;
&lt;h1&gt;Why use generators instead of arrays?&lt;/h1&gt;
&lt;p&gt;A traditional data provider might look like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-modifier&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;provideCases&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;array&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;string&quot;&gt;&amp;#39;case A&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;input-a&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;expected-a&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;string&quot;&gt;&amp;#39;case B&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;input-b&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;expected-b&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works, but generators give you a few advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No need to build a full array in memory&lt;/li&gt;
&lt;li&gt;Named cases are more natural with &lt;code&gt;yield&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Easier to compose or split into smaller logical chunks&lt;/li&gt;
&lt;li&gt;Better readability when cases grow&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;A simple example&lt;/h1&gt;
&lt;p&gt;Imagine you’re testing a service that transforms input values:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;ValueTransformer&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$value&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;strtoupper&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$value&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of writing multiple test methods, you can use a generator-based data provider:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;PHPUnit&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Framework&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Attributes&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;DataProvider&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;PHPUnit&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Framework&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Attributes&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;ValueTransformerTest&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;TestCase&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;DataProvider&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;provideTransformCases&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;it_transforms_values&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$input&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$expected&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$service&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;ValueTransformer&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$service&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$input&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;        &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertSame&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$expected&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-modifier&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;provideTransformCases&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Generator&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;        &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;lowercase&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;hello&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;HELLO&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;        &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;mixed case&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;HeLLo&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;HELLO&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;        &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;already uppercase&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;WORLD&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;WORLD&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Making test cases more expressive&lt;/h1&gt;
&lt;p&gt;Generators really shine when your inputs are objects instead of primitives.&lt;/p&gt;
&lt;p&gt;For example, testing a query modifier:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;QueryModifier&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;QueryBuilder&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$query&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$field&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$direction&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;QueryBuilder&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$query&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;orderBy&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$field&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$direction&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your test can focus on behavior while the generator defines the variations:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;DataProvider&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;provideSortingCases&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;it_applies_sorting&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$field&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$direction&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$query&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Mockery&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;mock&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;QueryBuilder&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$query&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;shouldReceive&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;orderBy&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;once&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$field&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$direction&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;andReturnSelf&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$modifier&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;QueryModifier&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$modifier&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$query&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$field&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$direction&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-modifier&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;provideSortingCases&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Generator&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;sort by name&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;asc&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;sort by created_at desc&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;created_at&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;desc&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;sort by nested field&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;user.email&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;asc&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The test stays minimal, while the generator clearly documents the supported scenarios.&lt;/p&gt;
&lt;h1&gt;Composing generators&lt;/h1&gt;
&lt;p&gt;One underrated benefit is that generators can be composed. You can split cases into smaller methods and reuse them:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-modifier&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;provideSortingCases&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Generator&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;yield from&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;basicFields&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;yield from&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;nestedFields&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;keyword-modifier&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;basicFields&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Generator&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;name asc&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;asc&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;created_at desc&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;created_at&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;desc&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;keyword-modifier&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;nestedFields&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Generator&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;user email&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;user.email&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;asc&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps large test suites maintainable without sacrificing clarity.&lt;/p&gt;
&lt;h1&gt;When to prefer generators&lt;/h1&gt;
&lt;p&gt;Generators are especially useful when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You have many test cases&lt;/li&gt;
&lt;li&gt;Test data is constructed dynamically&lt;/li&gt;
&lt;li&gt;You want to group or reuse subsets of cases&lt;/li&gt;
&lt;li&gt;Memory usage might become a concern&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For small, static datasets, arrays are perfectly fine. But once your data providers grow, generators tend to scale much better.&lt;/p&gt;
&lt;h1&gt;Final thoughts&lt;/h1&gt;
&lt;p&gt;Using generators in PHPUnit data providers is a small change that improves readability and flexibility. Tests become easier to extend, and the intent of each case is clearer thanks to named yields.&lt;/p&gt;
&lt;p&gt;If you’re already relying on data providers, switching from arrays to generators is a low-effort improvement that pays off quickly in larger test suites.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/testing&quot;&gt;#testing&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-07T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/using-generators-for-phpunit-data-providers</id>
    <title>🐥 Using generators for PHPUnit data providers</title>
    <updated>2026-05-07T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/fixing-the-ondrej-nginx-ppa-403-error-on-laravel-forge-servers" rel="alternate"/>
    <content type="html">&lt;p&gt;If you&apos;re running Ubuntu 22.04 on a Laravel Forge-managed server, you may have recently started seeing this error during &lt;code&gt;apt update&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;Err:7 https://ppa.launchpadcontent.net/ondrej/nginx/ubuntu jammy InRelease
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  403  Forbidden
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;E: The repository &amp;#39;https://ppa.launchpadcontent.net/ondrej/nginx/ubuntu jammy InRelease&amp;#39; is no longer signed.
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;What&apos;s happening?&lt;/h1&gt;
&lt;p&gt;Forge historically added the &lt;code&gt;ondrej/nginx&lt;/code&gt; PPA to its servers to get newer versions of nginx. Ondrej Surý (the maintainer) has since moved this PPA to a &lt;strong&gt;paid subscription model&lt;/strong&gt;, so unauthenticated access now returns a 403. Your server still has the old PPA entry, but it can no longer reach it.&lt;/p&gt;
&lt;p&gt;You&apos;ll notice the &lt;code&gt;ondrej/php&lt;/code&gt; PPA still works fine — that one remains freely available.&lt;/p&gt;
&lt;h1&gt;The fix&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Find the file that references the broken PPA:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;ondrej/nginx&quot;&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/etc/apt/sources.list.d/&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Remove it (the filename will match what grep returned above):&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/etc/apt/sources.list.d/ondrej-ubuntu-nginx-jammy.list&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/etc/apt/sources.list.d/ondrej-ubuntu-nginx-jammy.list.distUpgrade&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: &lt;code&gt;add-apt-repository --remove ppa:ondrej/nginx&lt;/code&gt; won&apos;t work here — Launchpad returns a 403 before it can resolve the PPA name.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Run &lt;code&gt;apt update&lt;/code&gt; again. If you need a newer version of nginx than Ubuntu&apos;s default (1.18.x), add the official nginx.org repository instead:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-fsSL&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;https://nginx.org/keys/nginx_signing.key&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;gpg&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--dearmor&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/usr/share/keyrings/nginx-archive-keyring.gpg&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/ubuntu jammy nginx&quot;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;tee&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/etc/apt/sources.list.d/nginx.list&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;update&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;nginx&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Do you need a newer nginx?&lt;/h1&gt;
&lt;p&gt;For most Laravel applications, Ubuntu&apos;s default nginx is perfectly adequate. Unless you have a specific reason to run a cutting-edge nginx version, removing the PPA and sticking with the default is the simplest and most stable path.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/devops&quot;&gt;#devops&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/linux&quot;&gt;#linux&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sysadmin&quot;&gt;#sysadmin&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-05T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/fixing-the-ondrej-nginx-ppa-403-error-on-laravel-forge-servers</id>
    <title>🐥 Fixing the Ondrej Nginx PPA 403 error on Laravel Forge servers</title>
    <updated>2026-05-05T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/upgrading-firebase-php-jwt-to-v7-in-a-laravel-app-transitive-dependency-trap" rel="alternate"/>
    <content type="html">&lt;p&gt;A few weeks ago we upgraded &lt;code&gt;firebase/php-jwt&lt;/code&gt; from v6 to v7 in our app. It sounds like a routine dependency bump — update the version constraint in &lt;code&gt;composer.json&lt;/code&gt;, run &lt;code&gt;composer update&lt;/code&gt;, done. But there was a catch: one of our other packages, &lt;code&gt;socialiteproviders/microsoft&lt;/code&gt;, had a hard constraint on the old version. This is the classic &lt;strong&gt;transitive dependency conflict&lt;/strong&gt;, and if you haven&apos;t run into it before, you will.&lt;/p&gt;
&lt;p&gt;Here&apos;s how we handled it and how you should too.&lt;/p&gt;
&lt;h1&gt;What&apos;s a transitive dependency conflict?&lt;/h1&gt;
&lt;p&gt;Your &lt;code&gt;composer.json&lt;/code&gt; only lists the packages you directly depend on. But each of those packages has its own &lt;code&gt;composer.json&lt;/code&gt;, listing &lt;em&gt;their&lt;/em&gt; dependencies — and those packages have their own, and so on. This tree is your full dependency graph.&lt;/p&gt;
&lt;p&gt;A transitive dependency conflict happens when you want to upgrade Package A, but Package B — which you also require — has a hard constraint that prevents it from working with the new version of A.&lt;/p&gt;
&lt;p&gt;In our case:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We wanted: &lt;code&gt;firebase/php-jwt: ^7.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;socialiteproviders/microsoft&lt;/code&gt; (v4.7.1) declared: &lt;code&gt;&quot;firebase/php-jwt&quot;: &quot;^6.8&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These two constraints are mutually exclusive. Composer can&apos;t install a version of &lt;code&gt;firebase/php-jwt&lt;/code&gt; that satisfies both &lt;code&gt;^7.0&lt;/code&gt; and &lt;code&gt;^6.8&lt;/code&gt; simultaneously, so it would refuse — with a dependency resolution error.&lt;/p&gt;
&lt;h1&gt;Step 1: Understand the dependency graph before touching anything&lt;/h1&gt;
&lt;p&gt;The first move is to inspect what the problematic package actually requires:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;composer&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;show&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;socialiteproviders/microsoft&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This outputs the &lt;code&gt;requires&lt;/code&gt; section:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;requires&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;php&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;^8.0&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;ext-json&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;*&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;firebase/php-jwt&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;^6.8&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;socialiteproviders/manager&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;^4.4&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There it is. Before writing a single line, you know exactly what needs to change.&lt;/p&gt;
&lt;h1&gt;Step 2: Validate with a dry run&lt;/h1&gt;
&lt;p&gt;Rather than guessing whether a combination of versions will resolve, ask Composer directly using &lt;code&gt;--dry-run&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;composer&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--dry-run&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;firebase/php-jwt:^7.0&quot;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;socialiteproviders/microsoft:^4.9&quot;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Composer will either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Print a clean resolution plan with the exact versions it would install, or&lt;/li&gt;
&lt;li&gt;Print an error telling you exactly which package is blocking and why&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In our case it resolved cleanly:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;Lock&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;operations:&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;installs,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;updates,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;removals&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;Upgrading&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;firebase/php-jwt&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;6.x-dev&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;string-special-path&quot;&gt;v7.0.5&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;Upgrading&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;socialiteproviders/microsoft&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;4.7.1&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;string-special-path&quot;&gt;4.9.1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing was written to disk. We just confirmed the upgrade path works.&lt;/p&gt;
&lt;h1&gt;Step 3: Update both packages in a single command&lt;/h1&gt;
&lt;p&gt;This is the part people get wrong. If you only update &lt;code&gt;firebase/php-jwt&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# ❌ This will fail — socialiteproviders/microsoft still requires ^6.8&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;composer&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;firebase/php-jwt&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Composer will refuse because the new &lt;code&gt;firebase/php-jwt&lt;/code&gt; v7 conflicts with the installed &lt;code&gt;socialiteproviders/microsoft&lt;/code&gt; v4.7.1. You must update both in one pass so Composer can solve the graph holistically:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# ✅ Composer solves both constraints together&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;composer&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;firebase/php-jwt&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;socialiteproviders/microsoft&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;Upgrading&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;firebase/php-jwt&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;6.x-dev&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;string-special-path&quot;&gt;v7.0.5&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;: &lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;Extracting&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;archive&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;Upgrading&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;socialiteproviders/microsoft&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;4.7.1&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;string-special-path&quot;&gt;4.9.1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;: &lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;Extracting&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;archive&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;No&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;security&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;vulnerability&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;advisories&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;found.&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Step 4: Bump the constraint in composer.json&lt;/h1&gt;
&lt;p&gt;Don&apos;t forget to update your version constraints in &lt;code&gt;composer.json&lt;/code&gt; to reflect the new minimums:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-json&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;text&quot;&gt;&quot;firebase/php-jwt&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; &quot;^7.0&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&quot;socialiteproviders/microsoft&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; &quot;^4.9&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why &lt;code&gt;^4.9&lt;/code&gt; instead of leaving it at &lt;code&gt;^4.1&lt;/code&gt;? Because &lt;code&gt;^4.9&lt;/code&gt; documents intent: &quot;we need at least 4.9, because that&apos;s the version that supports &lt;code&gt;firebase/php-jwt&lt;/code&gt; v7.&quot; If you leave it at &lt;code&gt;^4.1&lt;/code&gt;, a future &lt;code&gt;composer update&lt;/code&gt; could theoretically resolve back to 4.7.x in some edge case and silently reintroduce the conflict.&lt;/p&gt;
&lt;h1&gt;Step 5: Check your own code for API breakage&lt;/h1&gt;
&lt;p&gt;A major version bump means potential breaking changes. Grep for every place you use the package:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Firebase\\JWT&quot;&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;app/&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--include=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;*.php&quot;&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-l&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For &lt;code&gt;firebase/php-jwt&lt;/code&gt; specifically, &lt;code&gt;JWT::encode()&lt;/code&gt; kept the same signature in v7 — the only notable change was a stricter PHP type hint on the &lt;code&gt;$key&lt;/code&gt; parameter (&lt;code&gt;OpenSSLAsymmetricKey|OpenSSLCertificate|string&lt;/code&gt; vs the old loosely-typed &lt;code&gt;$key&lt;/code&gt;). Since we pass strings (shared secrets and RSA private keys), there was nothing to change in our &lt;code&gt;ZendeskJwtTokenBuilder&lt;/code&gt; or &lt;code&gt;DocuSignJwtTokenBuilder&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;Step 6: Run the affected tests&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;constant&quot;&gt;XDEBUG_MODE&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;off&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;php&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;artisan&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--compact&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;tests/Unit/Domains/Tokens/JwtTokenBuilderTest.php&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;tests/Feature/Authentication/MicrosoftLoginTest.php&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;Tests:&lt;/span&gt;    &lt;span class=&quot;number&quot;&gt;27&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;passed&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;106&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;assertions&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;Duration:&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;10.44s&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Green. Ship it.&lt;/p&gt;
&lt;h1&gt;The general playbook&lt;/h1&gt;
&lt;p&gt;Whenever you hit a transitive dependency conflict in Composer:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;composer show &lt;blocking-package&gt;&lt;/code&gt; — find the conflicting constraint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;composer require --dry-run &quot;a:^X&quot; &quot;b:^Y&quot;&lt;/code&gt; — validate resolution without touching files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;composer update a b&lt;/code&gt; — update all conflicting packages in one pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Tighten the version constraint in &lt;code&gt;composer.json&lt;/code&gt; to document the new minimum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Grep for usages, read the changelog for breaking API changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Run the affected tests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The core insight: &lt;strong&gt;Composer&apos;s constraint solver needs to see the full picture&lt;/strong&gt;. If you update one package at a time when there&apos;s a conflict, you&apos;re fighting the solver instead of working with it.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-04-24T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/upgrading-firebase-php-jwt-to-v7-in-a-laravel-app-transitive-dependency-trap</id>
    <title>🐥 Upgrading firebase/php-jwt to v7 in a Laravel App (transitive dependency trap)</title>
    <updated>2026-04-24T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/installing-claude-code-on-macos-with-homebrew-and-getting-the-latest-version" rel="alternate"/>
    <content type="html">&lt;p&gt;If you’re installing Claude Code on macOS using Homebrew, the official instruction currently suggests:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;claude-code&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While this works, it installs the &lt;strong&gt;latest stable version&lt;/strong&gt;, not the &lt;strong&gt;latest available version&lt;/strong&gt;. Depending on your use case, that can leave you behind on features and fixes.&lt;/p&gt;
&lt;h1&gt;Install the latest version&lt;/h1&gt;
&lt;p&gt;To install the most recent release, you should explicitly use:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;claude-code@latest&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures you’re running the newest version instead of the lagging stable cask.&lt;/p&gt;
&lt;h1&gt;Fix an existing installation&lt;/h1&gt;
&lt;p&gt;If you already installed &lt;code&gt;claude-code&lt;/code&gt; using the default command, you’ll need to replace it:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;uninstall&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;claude-code&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;claude-code@latest&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That removes the stable version and installs the latest one cleanly.&lt;/p&gt;
&lt;h1&gt;Why this matters&lt;/h1&gt;
&lt;p&gt;Homebrew distinguishes between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stable casks&lt;/strong&gt; → default installs (&lt;code&gt;claude-code&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Versioned or alternative casks&lt;/strong&gt; → explicit installs (&lt;code&gt;claude-code@latest&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this case, the naming is a bit misleading because &lt;code&gt;@latest&lt;/code&gt; is not the default. This has already caused confusion in the community and is being discussed upstream.&lt;/p&gt;
&lt;h1&gt;More context&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Issue discussion: &lt;a href=&quot;https://github.com/anthropics/claude-code/issues/42176&quot;&gt;https://github.com/anthropics/claude-code/issues/42176&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Homebrew PR: &lt;a href=&quot;https://github.com/Homebrew/homebrew-cask/pull/255221&quot;&gt;https://github.com/Homebrew/homebrew-cask/pull/255221&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Takeaway&lt;/h1&gt;
&lt;p&gt;If you want the newest Claude Code features and fixes, don’t rely on the default install. Use:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;claude-code@latest&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and you’ll avoid running an outdated version without realizing it.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/announcement&quot;&gt;#announcement&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/mac&quot;&gt;#mac&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-04-02T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/installing-claude-code-on-macos-with-homebrew-and-getting-the-latest-version</id>
    <title>🐥 Installing Claude Code on macOS with Homebrew (and getting the latest version)</title>
    <updated>2026-04-02T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/taming-scrollbars-in-a-phoenix-app" rel="alternate"/>
    <content type="html">&lt;p&gt;One of those tiny UI details that quietly annoys users more than you&apos;d expect: scrollbars that flash in and out of existence, causing the page layout to jump around. This week I finally cleaned it up in my Phoenix app and the fix was surprisingly elegant.&lt;/p&gt;
&lt;h1&gt;The problem&lt;/h1&gt;
&lt;p&gt;The original &lt;code&gt;root.html.heex&lt;/code&gt; had this on the &lt;code&gt;&lt;html&gt;&lt;/code&gt; element:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-html&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;html&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;en&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;[scrollbar-gutter:stable]&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;scrollbar-gutter: stable&lt;/code&gt; is a CSS property that reserves space for the scrollbar even when it isn&apos;t needed — the idea being to prevent layout shifts when content height changes. It&apos;s a reasonable approach, but it has a side effect: on macOS with &quot;Show scroll bars: Always&quot;, you end up with a permanent empty gutter on pages that don&apos;t scroll. On Windows, where scrollbars are visible by default, the gutter is always there. Depending on your layout, that reserved space can look odd.&lt;/p&gt;
&lt;h1&gt;The solution&lt;/h1&gt;
&lt;p&gt;I removed the Tailwind utility class from the &lt;code&gt;&lt;html&gt;&lt;/code&gt; tag and instead reached for a small CSS snippet:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-css&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;/* Only show scrollbars when content actually overflows */&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;property&quot;&gt;scrollbar-width&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; thin&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;property&quot;&gt;scrollbar-color&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; transparent transparent&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;hover&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;property&quot;&gt;scrollbar-color&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number-float&quot;&gt;0.2&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; transparent&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What this does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;scrollbar-width: thin&lt;/code&gt;&lt;/strong&gt; — uses the browser&apos;s thin scrollbar variant, which is less visually heavy than the default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;scrollbar-color: transparent transparent&lt;/code&gt;&lt;/strong&gt; — hides the scrollbar thumb and track by making them fully transparent. The scrollbar doesn&apos;t disappear from the DOM; it just becomes invisible when you&apos;re not interacting with the element.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;*:hover &amp;lbrace; scrollbar-color: rgba(0,0,0,0.2) transparent &amp;rbrace;&lt;/code&gt;&lt;/strong&gt; — fades the scrollbar thumb in (as a subtle translucent grey) only when the user hovers over the element. This gives a clean overlay-style scrollbar feel, similar to what macOS does natively.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Why this feels better&lt;/h1&gt;
&lt;p&gt;The end result is a UI that looks clean and uncluttered at rest, but still gives users a clear scrollbar affordance the moment they move their cursor over a scrollable area. No layout jump, no reserved gutter space, and no permanently visible chrome competing for attention.&lt;/p&gt;
&lt;p&gt;It&apos;s a two-file change — one CSS block and the removal of a single Tailwind class — but the visual impact is noticeable, especially on pages with sidebars or nested scrollable containers.&lt;/p&gt;
&lt;h1&gt;Browser support&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;scrollbar-width&lt;/code&gt; and &lt;code&gt;scrollbar-color&lt;/code&gt; are part of the &lt;a href=&quot;https://www.w3.org/TR/css-scrollbars-1/&quot;&gt;CSS Scrollbars Specification&lt;/a&gt; and have solid support in Firefox and Chromium-based browsers. Safari added support in version 18.2. For older Safari versions the scrollbar will just render with its default appearance — a perfectly acceptable fallback.&lt;/p&gt;
&lt;p&gt;Small change, cleaner app. Sometimes that&apos;s all it takes.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/css&quot;&gt;#css&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/html&quot;&gt;#html&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-31T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/taming-scrollbars-in-a-phoenix-app</id>
    <title>🐥 Taming scrollbars in a Phoenix app</title>
    <updated>2026-03-31T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/a-better-way-using-mkcert-for-https-in-phoenix-on-macos" rel="alternate"/>
    <content type="html">&lt;p&gt;In a previous post, I showed how to use &lt;code&gt;mix phx.gen.cert&lt;/code&gt; to set up HTTPS in Phoenix development. While that approach works in theory, in practice it&apos;s a minefield: OpenSSL 3.x generates PKCS12 bundles that macOS&apos;s &lt;code&gt;security&lt;/code&gt; command rejects, browsers send cryptic &lt;code&gt;Decode Error&lt;/code&gt; alerts, and manually trusting certificates in Keychain Access often has no effect at all.&lt;/p&gt;
&lt;p&gt;There&apos;s a much better tool for this: &lt;a href=&quot;https://github.com/FiloSottile/mkcert&quot;&gt;mkcert&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;What makes mkcert different?&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;mkcert&lt;/code&gt; creates a local Certificate Authority (CA) on your machine and registers it with macOS&apos;s system trust store, Firefox, and Chrome in one command. Any certificate you generate from it is automatically trusted — no manual Keychain fiddling required.&lt;/p&gt;
&lt;h1&gt;Step 1: Install mkcert and register the local CA&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;mkcert&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;mkcert&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-install&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;-install&lt;/code&gt; step is what makes everything work. It adds mkcert&apos;s root CA to your system keychain so all browsers trust it going forward.&lt;/p&gt;
&lt;p&gt;Verify it landed:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;security&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;find-certificate&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;mkcert&quot;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Step 2: Generate a certificate for localhost&lt;/h1&gt;
&lt;p&gt;From your Phoenix project root:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;mkcert&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-cert-file&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;priv/cert/selfsigned.pem&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;       &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;-key-file&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;priv/cert/selfsigned_key.pem&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;       &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;localhost&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;127.0.0.1&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;::1&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This generates a certificate valid for &lt;code&gt;localhost&lt;/code&gt;, &lt;code&gt;127.0.0.1&lt;/code&gt;, and &lt;code&gt;::1&lt;/code&gt;, signed by your local CA.&lt;/p&gt;
&lt;h1&gt;Step 3: Configure Phoenix for HTTPS&lt;/h1&gt;
&lt;p&gt;Update &lt;code&gt;config/dev.exs&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:your_app&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;YourAppWeb.Endpoint&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;https: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;port: &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;4001&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;cipher_suite: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:strong&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;certfile: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;priv/cert/selfsigned.pem&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;keyfile: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;priv/cert/selfsigned_key.pem&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;check_origin: &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;code_reloader: &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;debug_errors: &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start your server:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;mix&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;phx.server&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Visit &lt;code&gt;https://localhost:4001&lt;/code&gt; — no browser warnings, no certificate errors, no Keychain gymnastics.&lt;/p&gt;
&lt;h1&gt;What about the cert files in version control?&lt;/h1&gt;
&lt;p&gt;The generated &lt;code&gt;priv/cert/&lt;/code&gt; files are already in &lt;code&gt;.gitignore&lt;/code&gt; when using &lt;code&gt;mix phx.gen.cert&lt;/code&gt;, and should stay there with mkcert too. Each developer on your team runs &lt;code&gt;mkcert -install&lt;/code&gt; and generates their own certificate locally.&lt;/p&gt;
&lt;h1&gt;Upgrading from the old approach&lt;/h1&gt;
&lt;p&gt;If you followed the previous post, you can replace the existing cert files in place — the Phoenix config stays the same since you&apos;re still pointing at &lt;code&gt;priv/cert/selfsigned.pem&lt;/code&gt; and &lt;code&gt;priv/cert/selfsigned_key.pem&lt;/code&gt;. Just regenerate them with mkcert and restart your server.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/terminal&quot;&gt;#terminal&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-30T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/a-better-way-using-mkcert-for-https-in-phoenix-on-macos</id>
    <title>🐥 A better way: Using mkcert for HTTPS in Phoenix on macOS</title>
    <updated>2026-03-30T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/fixing-a-race-condition-in-oban-job-counting-with-telemetry" rel="alternate"/>
    <content type="html">&lt;p&gt;When building a LiveView dashboard that shows how many background jobs are still processing, a subtle race condition can make the count permanently off by one. Here&apos;s how I ran into it and how Oban&apos;s telemetry system solved it cleanly.&lt;/p&gt;
&lt;h1&gt;The Setup&lt;/h1&gt;
&lt;p&gt;The app has an Oban worker — &lt;code&gt;ProcessExternalLinkWorker&lt;/code&gt; — that fetches a URL, extracts content, and creates a post. The LiveView index page shows a &quot;X post(s) currently being processed&quot; banner while jobs are in flight.&lt;/p&gt;
&lt;p&gt;The job count is a straightforward Oban query:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Job&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;where: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;not in&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;completed&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;discarded&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;worker&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;worker&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;aggregate&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:count&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The LiveView subscribes to a &lt;code&gt;&quot;posts&quot;&lt;/code&gt; PubSub topic and refreshes this count whenever a &lt;code&gt;&quot;post_updated&quot;&lt;/code&gt; message arrives. That message is broadcast from inside &lt;code&gt;Posts.create_post/1&lt;/code&gt;, which is called from within the worker.&lt;/p&gt;
&lt;h1&gt;The Bug&lt;/h1&gt;
&lt;p&gt;Here&apos;s the execution sequence that causes the problem:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Oban picks up a job — state transitions to &lt;code&gt;executing&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The worker calls &lt;code&gt;ProcessExternalLink.process_url/1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;That calls &lt;code&gt;Posts.create_post/1&lt;/code&gt;, which broadcasts &lt;code&gt;&quot;post_updated&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The LiveView receives the broadcast and re-queries the job count&lt;/li&gt;
&lt;li&gt;The job is &lt;strong&gt;still &lt;code&gt;executing&lt;/code&gt;&lt;/strong&gt; — it hasn&apos;t returned &lt;code&gt;:ok&lt;/code&gt; yet&lt;/li&gt;
&lt;li&gt;The count includes this job, showing 1 item &quot;still processing&quot; even though the work is done&lt;/li&gt;
&lt;li&gt;The worker returns &lt;code&gt;:ok&lt;/code&gt;, Oban marks the job &lt;code&gt;completed&lt;/code&gt; — but no one tells the LiveView&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The post list refreshes correctly, but the processing counter stays at 1 until the next page load.&lt;/p&gt;
&lt;p&gt;Subtracting 1 from the count isn&apos;t a fix — with multiple concurrent jobs, you&apos;d need to know exactly how many are in this &quot;just finished broadcasting but not yet completed&quot; state, which is unknowable from the outside.&lt;/p&gt;
&lt;h1&gt;The Fix: Oban Telemetry&lt;/h1&gt;
&lt;p&gt;Oban emits telemetry events throughout the job lifecycle. The key one here is &lt;code&gt;[:oban, :job, :stop]&lt;/code&gt;, which fires &lt;strong&gt;after&lt;/strong&gt; the job state has been updated to &lt;code&gt;completed&lt;/code&gt; in the database. There&apos;s also &lt;code&gt;[:oban, :job, :exception]&lt;/code&gt; for failed jobs.&lt;/p&gt;
&lt;p&gt;The fix is to decouple the job count refresh from the &lt;code&gt;&quot;post_updated&quot;&lt;/code&gt; broadcast. Instead, attach a telemetry handler that broadcasts a separate &lt;code&gt;&quot;jobs_updated&quot;&lt;/code&gt; message when a job finishes:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyWebApp.ObanTelemetryHandler&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Logger&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;attach&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;type&quot;&gt;:telemetry&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;detach&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;oban-job-lifecycle&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;type&quot;&gt;:telemetry&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;attach_many&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;      &lt;span class=&quot;string&quot;&gt;&quot;oban-job-lifecycle&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:oban&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:job&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:stop&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:oban&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:job&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:exception&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;      &lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;constant-builtin&quot;&gt;__MODULE__&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function&quot;&gt;handle_event&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;      &lt;span class=&quot;constant-builtin&quot;&gt;nil&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;handle_event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:oban&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:job&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_measurements&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;meta&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_config&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;module&quot;&gt;Logger&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;debug&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;Oban job &lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;#&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;: worker=&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;#&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;meta&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:worker&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;    &lt;span class=&quot;module&quot;&gt;Phoenix.PubSub&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;broadcast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;MyWebApp.PubSub&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;posts&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;event: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;jobs_updated&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two design decisions worth noting:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;detach&lt;/code&gt; before &lt;code&gt;attach&lt;/code&gt;&lt;/strong&gt;: Calling &lt;code&gt;attach_many&lt;/code&gt; with a handler ID that&apos;s already registered raises an &lt;code&gt;ArgumentError&lt;/code&gt;. In development, a full server restart re-runs &lt;code&gt;application.ex&lt;/code&gt; and would hit this error on the second start. Calling &lt;code&gt;detach&lt;/code&gt; first makes &lt;code&gt;attach/0&lt;/code&gt; idempotent at the cost of one no-op call.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No worker filter&lt;/strong&gt;: An earlier version filtered on the worker name in the handler&apos;s pattern match. That&apos;s redundant — the Ecto query in the LiveView already scopes the count to the specific worker. Removing the filter keeps the handler simpler and avoids fragility around how Oban formats the worker name in telemetry metadata.&lt;/p&gt;
&lt;p&gt;Call &lt;code&gt;attach/0&lt;/code&gt; in &lt;code&gt;application.ex&lt;/code&gt; after the supervisor starts:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Supervisor&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;start_link&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;children&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;module&quot;&gt;MyWebApp.ObanTelemetryHandler&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;attach&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then handle the new event in the LiveView, separate from &lt;code&gt;&quot;post_updated&quot;&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;handle_info&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;event: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;jobs_updated&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;worker&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;MyWebApp.Workers.ProcessExternalLinkWorker&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;num_processing&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Job&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;where: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;not in&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;completed&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;discarded&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;worker&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;worker&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;aggregate&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:count&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;assign&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:num_processing&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;num_processing&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Why This Works&lt;/h1&gt;
&lt;p&gt;The &lt;code&gt;&quot;post_updated&quot;&lt;/code&gt; broadcast still fires mid-job and the post list still refreshes correctly — that part was never broken. But the job count is now only refreshed in response to the telemetry event, which is guaranteed to fire after the state change has been committed. The LiveView queries at the right moment.&lt;/p&gt;
&lt;p&gt;It also handles failure correctly. If the worker raises an exception, &lt;code&gt;[:oban, :job, :exception]&lt;/code&gt; fires, the LiveView refreshes the count, and any retryable or discarded jobs show up accurately.&lt;/p&gt;
&lt;h1&gt;Takeaway&lt;/h1&gt;
&lt;p&gt;When displaying counts or status derived from job state, don&apos;t trigger the refresh from within the job itself. The job is still running at that point. Instead, hook into Oban&apos;s telemetry events, which fire at well-defined points in the lifecycle after state transitions have been committed.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-29T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/fixing-a-race-condition-in-oban-job-counting-with-telemetry</id>
    <title>🐥 Fixing a race condition in Oban job counting with telemetry</title>
    <updated>2026-03-29T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/til-filtering-github-prs-that-are-ready-for-review-and-not-yours" rel="alternate"/>
    <content type="html">&lt;p&gt;When reviewing pull requests in GitHub, I often want a clean list of PRs that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;are open&lt;/li&gt;
&lt;li&gt;are not drafts&lt;/li&gt;
&lt;li&gt;are not created by me&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s the filter I use:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;sort:updated-desc is:pr is:open -author:username draft:false
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;What this does&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sort:updated-desc&lt;/code&gt; → most recently updated PRs first&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is:pr&lt;/code&gt; → only pull requests&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is:open&lt;/code&gt; → only open PRs&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-author:username&lt;/code&gt; → exclude your own PRs&lt;/li&gt;
&lt;li&gt;&lt;code&gt;draft:false&lt;/code&gt; → exclude draft PRs (only show ready-for-review)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This gives a focused, high-signal list of PRs that are actually actionable.&lt;/p&gt;
&lt;h1&gt;Bonus: explore more filters&lt;/h1&gt;
&lt;p&gt;GitHub’s search syntax is surprisingly powerful. You can filter by labels, review status, checks, branches, and more.&lt;/p&gt;
&lt;p&gt;Check out the full reference here:
&lt;a href=&quot;https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests&quot;&gt;https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A few useful additions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;review-requested:@me&lt;/code&gt; → PRs requesting your review&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status:success&lt;/code&gt; → only PRs with passing checks&lt;/li&gt;
&lt;li&gt;&lt;code&gt;label:bug&lt;/code&gt; → filter by label&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-is:merged&lt;/code&gt; → exclude merged PRs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you start combining these, you can build very tailored review dashboards directly in GitHub.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/github&quot;&gt;#github&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-28T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/til-filtering-github-prs-that-are-ready-for-review-and-not-yours</id>
    <title>🐥 TIL: filtering GitHub PRs that are ready for review and not yours</title>
    <updated>2026-03-28T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/enforcing-polymorphic-integrity-in-postgresql-with-num-nonnulls" rel="alternate"/>
    <content type="html">&lt;p&gt;Polymorphic associations are common when a single table can reference multiple other tables. A typical implementation is one table with multiple nullable foreign keys:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;my_poly_assocs&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;bigserial&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;assoc_a_id&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;bigint&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;REFERENCES&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;assoc_a&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;assoc_b_id&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;bigint&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;REFERENCES&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;assoc_b&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;assoc_c_id&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;bigint&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;REFERENCES&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;assoc_c&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The intent is simple: each row should reference &lt;strong&gt;exactly one&lt;/strong&gt; of these associations. But the database won’t enforce that automatically. Without extra constraints, you can end up with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No association set (all NULL)&lt;/li&gt;
&lt;li&gt;Multiple associations set (invalid state)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PostgreSQL has a clean solution for this.&lt;/p&gt;
&lt;h1&gt;The &lt;code&gt;num_nonnulls&lt;/code&gt; function&lt;/h1&gt;
&lt;p&gt;PostgreSQL provides a built-in function called &lt;code&gt;num_nonnulls&lt;/code&gt;. It returns the number of arguments that are not NULL.&lt;/p&gt;
&lt;p&gt;That makes it perfect for enforcing “exactly one” semantics:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;my_poly_assocs&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;ADD&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;CONSTRAINT&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; exactly_one_assoc_referenced
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;CHECK&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;num_nonnulls&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;assoc_a_id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;assoc_b_id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;assoc_c_id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This constraint guarantees:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;At least one foreign key is set&lt;/li&gt;
&lt;li&gt;No more than one foreign key is set&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a row violates the rule, the insert or update fails immediately.&lt;/p&gt;
&lt;h1&gt;Why this is better than application-level checks&lt;/h1&gt;
&lt;p&gt;You could enforce this rule in your application layer, but that leaves room for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Race conditions&lt;/li&gt;
&lt;li&gt;Multiple services writing to the same database&lt;/li&gt;
&lt;li&gt;Future code paths forgetting the rule&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A &lt;code&gt;CHECK&lt;/code&gt; constraint keeps the invariant inside the database, where it belongs.&lt;/p&gt;
&lt;h1&gt;Variations&lt;/h1&gt;
&lt;p&gt;If your requirement is “at most one” instead of “exactly one”, you can adjust the constraint:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;text&quot;&gt;CHECK &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;num_nonnulls&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;assoc_a_id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; assoc_b_id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; assoc_c_id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;=&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; 1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you later add a new polymorphic target, you must update the constraint to include the new column.&lt;/p&gt;
&lt;h1&gt;When to use this pattern&lt;/h1&gt;
&lt;p&gt;This approach works well when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You need strict relational integrity.&lt;/li&gt;
&lt;li&gt;The set of polymorphic targets is finite and known.&lt;/li&gt;
&lt;li&gt;You want predictable query performance without an additional type column.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your targets are dynamic or numerous, a more classic polymorphic design (e.g. &lt;code&gt;target_type&lt;/code&gt; + &lt;code&gt;target_id&lt;/code&gt;) may be more flexible, though it trades off referential integrity.&lt;/p&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;num_nonnulls&lt;/code&gt; is a small but powerful feature in PostgreSQL. It allows you to enforce a subtle but important invariant with a single CHECK constraint, keeping your polymorphic associations consistent and safe at the database level.&lt;/p&gt;
&lt;p&gt;It’s one of those features that feels obvious once you know it exists.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/postgresql&quot;&gt;#postgresql&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sql&quot;&gt;#sql&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-27T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/enforcing-polymorphic-integrity-in-postgresql-with-num-nonnulls</id>
    <title>🐥 Enforcing polymorphic integrity in PostgreSQL with num_nonnulls</title>
    <updated>2026-03-27T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/request-validation-in-phoenix-the-laravel-formrequest-approach" rel="alternate"/>
    <content type="html">&lt;p&gt;Developers coming from Laravel are used to &lt;strong&gt;FormRequest classes&lt;/strong&gt; that encapsulate request validation and authorization. A typical FormRequest contains validation rules, optional authorization logic, and automatically provides validated input to the controller.&lt;/p&gt;
&lt;p&gt;Phoenix takes a slightly different approach. Instead of request-focused validation objects, validation is typically handled using &lt;strong&gt;Ecto changesets&lt;/strong&gt;. This approach moves validation closer to the data model and keeps controllers thin.&lt;/p&gt;
&lt;p&gt;This article explains how validation works in Phoenix and how to implement reusable custom validation rules similar to Laravel.&lt;/p&gt;
&lt;h2&gt;Validation with Ecto changesets&lt;/h2&gt;
&lt;p&gt;In Phoenix, validation is usually implemented inside an Ecto changeset. A changeset handles three responsibilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;casting incoming parameters&lt;/li&gt;
&lt;li&gt;validating data&lt;/li&gt;
&lt;li&gt;collecting validation errors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A typical schema with validations looks like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Accounts.User&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Ecto.Schema&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Ecto.Changeset&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;schema&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;users&quot;&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;field&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:string&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;field&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:string&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;user&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_required&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_format&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;r/@/&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Incoming request data is passed to the changeset through a context function:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;create_user&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The controller then handles the result:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Accounts&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;create_user&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;      &lt;span class=&quot;function-call&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;      &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;put_status&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:unprocessable_entity&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;      &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;errors: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This already provides most of the functionality developers expect from Laravel FormRequests.&lt;/p&gt;
&lt;h2&gt;Writing custom validation rules&lt;/h2&gt;
&lt;p&gt;Custom validation logic in Phoenix is implemented as functions that operate on a changeset. These functions can be private helpers or reusable validation utilities.&lt;/p&gt;
&lt;h3&gt;Field-level custom validation&lt;/h3&gt;
&lt;p&gt;The most common tool for custom rules is &lt;code&gt;validate_change/3&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;validate_company_email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;validate_change&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;email&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;ends_with?&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;@company.com&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;else&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;email: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;must be a company email&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can include this in a changeset pipeline:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;user&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_required&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_company_email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the validation fails, an error is added to the changeset.&lt;/p&gt;
&lt;h3&gt;Reusable validation helpers&lt;/h3&gt;
&lt;p&gt;If validation logic should be reused across schemas, it can be extracted into a module.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Validations&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Ecto.Changeset&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;validate_company_email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;field&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;validate_change&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;field&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;field&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;email&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;ends_with?&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;@company.com&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;      &lt;span class=&quot;keyword&quot;&gt;else&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;field&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;must be a company email&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;      &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Usage inside a changeset:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Validations&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;user&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_required&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_company_email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern is similar to reusable validation rules in Laravel.&lt;/p&gt;
&lt;h3&gt;Cross-field validation&lt;/h3&gt;
&lt;p&gt;Some rules depend on multiple fields. In these cases, the changeset can be inspected directly.&lt;/p&gt;
&lt;p&gt;For example, validating a date range:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;validate_date_range&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;start_date&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;get_field&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:start_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;end_date&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;get_field&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:end_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;start_date&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;end_date&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;compare&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;start_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;end_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:gt&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;add_error&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:start_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;must be before end date&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;else&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Used inside a changeset:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;event&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:start_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:end_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_required&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:start_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:end_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_date_range&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Database-backed validation&lt;/h3&gt;
&lt;p&gt;Some validation rules depend on the database. For example, checking whether an email already exists.&lt;/p&gt;
&lt;p&gt;While this can be implemented manually, the preferred approach is to rely on database constraints.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;unique_constraint&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This requires a unique index in the database and prevents race conditions that can occur with manual checks.&lt;/p&gt;
&lt;h2&gt;Key building blocks&lt;/h2&gt;
&lt;p&gt;Custom validation in Ecto is built on a few core functions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;validate_change/3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add_error/3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_field/2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validate_required/2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validate_length/3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validate_format/3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validate_number/3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validate_inclusion/3&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most complex validation logic can be composed from these primitives.&lt;/p&gt;
&lt;h2&gt;Comparing Laravel and Phoenix validation&lt;/h2&gt;
&lt;p&gt;Laravel focuses validation around the HTTP request, while Phoenix places validation closer to the data layer.&lt;/p&gt;
&lt;p&gt;Laravel:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;module&quot;&gt;FormRequest&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  ↓
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;V&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;alidator&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  ↓
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;ontroller&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Phoenix:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;module&quot;&gt;Controller&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  ↓
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;ontext&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  ↓
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;hangeset&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This design makes validation reusable across:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP APIs&lt;/li&gt;
&lt;li&gt;Phoenix HTML forms&lt;/li&gt;
&lt;li&gt;LiveView forms&lt;/li&gt;
&lt;li&gt;background jobs&lt;/li&gt;
&lt;li&gt;internal application logic&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By attaching validation to the data structure instead of the request, Phoenix ensures consistent validation regardless of where data enters the system.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Phoenix does not provide a direct equivalent to Laravel FormRequests, but Ecto changesets offer a powerful and flexible alternative.&lt;/p&gt;
&lt;p&gt;Validation rules live alongside the data structure, are easily composable, and can be reused across different parts of the application. Custom rules are implemented as simple functions that transform changesets, making them easy to test and reuse.&lt;/p&gt;
&lt;p&gt;For developers moving from Laravel, the key mindset shift is moving validation from the request layer to the data layer. Once adopted, this pattern results in clean controllers, reusable validation logic, and consistent data integrity across the entire application.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/http&quot;&gt;#http&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-25T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/request-validation-in-phoenix-the-laravel-formrequest-approach</id>
    <title>🐥 Request validation in Phoenix: the Laravel FormRequest approach</title>
    <updated>2026-03-25T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/making-oban-workers-reusable-with-job-arguments" rel="alternate"/>
    <content type="html">&lt;p&gt;When you first write an Oban worker, it&apos;s tempting to hardcode its configuration directly in the module. A worker that
fetches an RSS feed might embed the URL as a module attribute. It works fine — until you need to do the same thing for a
second feed, and suddenly you&apos;re copy-pasting a nearly identical module.&lt;/p&gt;
&lt;p&gt;There&apos;s a better way.&lt;/p&gt;
&lt;h1&gt;The Problem: One Worker, One Purpose&lt;/h1&gt;
&lt;p&gt;A typical first pass at a feed-fetching worker looks something like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Workers.FetchThinkingElixirFeedWorker&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Oban.Worker&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;queue: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:default&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;max_attempts: &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;feed_url &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;https://www.yellowduck.be/posts/feed&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;tags &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;elixir&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;phoenix&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;impl &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Oban.Worker&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Oban.Job&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;# fetch and process @feed_url, apply @tags...&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is completely fine for one feed. But the moment you want to add a second feed, you&apos;re either duplicating the module
or reaching for inheritance patterns that don&apos;t belong here.&lt;/p&gt;
&lt;h1&gt;The Fix: Pass Configuration as Job Args&lt;/h1&gt;
&lt;p&gt;Oban jobs carry an args map that gets persisted alongside the job. Instead of hardcoding configuration in the module,
move it into those args:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Workers.FetchFeedWorker&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Oban.Worker&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;queue: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:default&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;max_attempts: &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;impl &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Oban.Worker&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Oban.Job&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;args: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;feed_url&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;feed_url&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;tags&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;# fetch and process feed_url, apply tags...&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the worker is a generic mechanism. The what (which feed, which tags) is data — not code.&lt;/p&gt;
&lt;h1&gt;Using It for Scheduled Jobs&lt;/h1&gt;
&lt;p&gt;The Oban cron plugin supports passing args directly to scheduled workers, so you get the same ergonomics for recurring
jobs:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Oban.Plugins.Cron&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;crontab: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;0 * * * *&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Workers.FetchFeedWorker&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;args: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;feed_url&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;https://www.yellowduck.be/posts/feed&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&quot;tags&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;elixir&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;phoenix&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;0 * * * *&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Workers.FetchFeedWorker&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;args: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;feed_url&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;https://changelog.com/podcast/feed&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&quot;tags&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;programming&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;open-source&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two cron entries, one worker module. Adding a third feed is a config change, not a code change.&lt;/p&gt;
&lt;h1&gt;Inserting One-Off Jobs&lt;/h1&gt;
&lt;p&gt;The same pattern works for manually enqueued jobs:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;feed_url&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;https://example.com/rss&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;tags&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;news&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Workers.FetchFeedWorker&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Oban&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Why This Matters&lt;/h1&gt;
&lt;p&gt;Less code to maintain. One module handles all feeds. Bug fixes and improvements apply everywhere automatically.&lt;/p&gt;
&lt;p&gt;Clearer separation of concerns. The worker encodes how to process a feed. The job args encode which feed to process.
These are genuinely different things and should live in different places.&lt;/p&gt;
&lt;p&gt;More observable. Because args are stored in the database with each job, you can see exactly what configuration ran —
useful when debugging why a particular job behaved a certain way.&lt;/p&gt;
&lt;p&gt;Easier to extend. Want to add a max_items option? Add it to the args map and pattern match on it with a default. No new
module required.&lt;/p&gt;
&lt;h1&gt;When to Keep Workers Specific&lt;/h1&gt;
&lt;p&gt;This pattern isn&apos;t always the right call. If two &quot;similar&quot; workers actually have meaningfully different logic —
different parsing strategies, different retry behaviour, different side effects — a shared module can become a tangle of
conditionals. In that case, separate modules with a shared private helper or a behaviour is often cleaner.&lt;/p&gt;
&lt;p&gt;But when the logic is truly the same and only the inputs differ, push the inputs into args and let the worker be a
function.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-23T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/making-oban-workers-reusable-with-job-arguments</id>
    <title>🐥 Making Oban workers reusable with job arguments</title>
    <updated>2026-03-23T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/understanding-agent-genserver-task-and-ets-in-elixir" rel="alternate"/>
    <content type="html">&lt;p&gt;When building concurrent systems in Elixir, you have several OTP abstractions available. Two of the most commonly discussed are &lt;code&gt;Agent&lt;/code&gt; and &lt;code&gt;GenServer&lt;/code&gt;, but in real systems developers also frequently use &lt;code&gt;Task&lt;/code&gt;, &lt;code&gt;ETS&lt;/code&gt;, and sometimes &lt;code&gt;GenStage&lt;/code&gt; or &lt;code&gt;Broadway&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Understanding when to use each abstraction is important for building systems that remain simple, scalable, and maintainable.&lt;/p&gt;
&lt;p&gt;This article explains the differences and ends with a common GenServer anti-pattern to avoid.&lt;/p&gt;
&lt;h1&gt;Agent: a simple state container&lt;/h1&gt;
&lt;p&gt;An &lt;code&gt;Agent&lt;/code&gt; is the simplest abstraction for managing shared state in a process.&lt;/p&gt;
&lt;p&gt;It wraps a process that holds state and provides helper functions to read or update that state.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Agent&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;start_link&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;module&quot;&gt;Agent&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;hello&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;module&quot;&gt;Agent&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Characteristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stores state in a separate process&lt;/li&gt;
&lt;li&gt;Minimal API (&lt;code&gt;get&lt;/code&gt; and &lt;code&gt;update&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;No message handling&lt;/li&gt;
&lt;li&gt;No lifecycle callbacks&lt;/li&gt;
&lt;li&gt;Very small abstraction&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Typical use cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Small in-memory caches&lt;/li&gt;
&lt;li&gt;Counters&lt;/li&gt;
&lt;li&gt;Temporary shared state&lt;/li&gt;
&lt;li&gt;Test helpers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;module&quot;&gt;Agent&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;start_link&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;MyCache&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;module&quot;&gt;Agent&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;MyCache&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;module&quot;&gt;Agent&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;MyCache&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An Agent is essentially &lt;strong&gt;a lightweight wrapper around a process holding state&lt;/strong&gt;.&lt;/p&gt;
&lt;h1&gt;GenServer: a full OTP server abstraction&lt;/h1&gt;
&lt;p&gt;A &lt;code&gt;GenServer&lt;/code&gt; is a behaviour for implementing long-running server processes.&lt;/p&gt;
&lt;p&gt;It provides a structured way to handle messages, maintain state, and react to events.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Counter&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;GenServer&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;start_link&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;initial&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;module&quot;&gt;GenServer&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;start_link&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constant-builtin&quot;&gt;__MODULE__&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;initial&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;constant-builtin&quot;&gt;__MODULE__&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;increment&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;module&quot;&gt;GenServer&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constant-builtin&quot;&gt;__MODULE__&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:increment&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;module&quot;&gt;GenServer&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constant-builtin&quot;&gt;__MODULE__&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:value&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;handle_cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:increment&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;handle_call&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:value&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_from&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:reply&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Characteristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Structured callbacks (&lt;code&gt;init&lt;/code&gt;, &lt;code&gt;handle_call&lt;/code&gt;, &lt;code&gt;handle_cast&lt;/code&gt;, &lt;code&gt;handle_info&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Supports synchronous and asynchronous communication&lt;/li&gt;
&lt;li&gt;Integrates with supervision trees&lt;/li&gt;
&lt;li&gt;Can schedule work and handle system messages&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Typical use cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stateful services&lt;/li&gt;
&lt;li&gt;Resource managers&lt;/li&gt;
&lt;li&gt;Background workers&lt;/li&gt;
&lt;li&gt;Caches with logic&lt;/li&gt;
&lt;li&gt;Rate limiters&lt;/li&gt;
&lt;li&gt;Schedulers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A GenServer is best thought of as &lt;strong&gt;a stateful actor that encapsulates behaviour&lt;/strong&gt;.&lt;/p&gt;
&lt;h1&gt;Why many developers skip Agent&lt;/h1&gt;
&lt;p&gt;Although Agents are simple, many production systems grow beyond their capabilities.&lt;/p&gt;
&lt;p&gt;Two common limitations are:&lt;/p&gt;
&lt;h3&gt;Business logic leaks outside the process&lt;/h3&gt;
&lt;p&gt;With Agents, the caller often contains the business logic:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;module&quot;&gt;Agent&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;module&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With a GenServer, the process owns the behaviour:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;increment&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;module&quot;&gt;GenServer&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constant-builtin&quot;&gt;__MODULE__&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:increment&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;handle_cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:increment&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes the process behave like a &lt;strong&gt;service with a well-defined API&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;Limited extensibility&lt;/h3&gt;
&lt;p&gt;Real systems often need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;periodic work&lt;/li&gt;
&lt;li&gt;cache expiration&lt;/li&gt;
&lt;li&gt;telemetry&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;batching&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are difficult to implement with Agents but natural in a GenServer.&lt;/p&gt;
&lt;p&gt;Because of this, many developers default to GenServer.&lt;/p&gt;
&lt;h1&gt;Task: concurrency for short-lived work&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;Task&lt;/code&gt; is designed for &lt;strong&gt;temporary concurrent work&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;variable&quot;&gt;task&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;fetch_feed&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;module&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For parallel workloads:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;variable&quot;&gt;urls&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;async_stream&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;function&quot;&gt;fetch_feed&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;max_concurrency: &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;to_list&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Typical use cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;parallel HTTP requests&lt;/li&gt;
&lt;li&gt;data processing&lt;/li&gt;
&lt;li&gt;CPU-bound work&lt;/li&gt;
&lt;li&gt;concurrent API calls&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tasks should be used for &lt;strong&gt;short-lived processes&lt;/strong&gt;, not long-running services.&lt;/p&gt;
&lt;h1&gt;ETS: extremely fast shared memory&lt;/h1&gt;
&lt;p&gt;ETS (Erlang Term Storage) is an in-memory storage system optimized for concurrent access.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;type&quot;&gt;:ets&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:cache&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:set&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:public&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:named_table&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;type&quot;&gt;:ets&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:cache&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:key&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;type&quot;&gt;:ets&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;lookup&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:cache&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Characteristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;extremely fast&lt;/li&gt;
&lt;li&gt;concurrent reads&lt;/li&gt;
&lt;li&gt;shared memory&lt;/li&gt;
&lt;li&gt;no process bottleneck&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A common pattern is:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;module&quot;&gt;GenServer&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;   ↓
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;ETS &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;table&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The GenServer manages lifecycle and policies, while ETS stores the data.&lt;/p&gt;
&lt;h1&gt;A common GenServer anti-pattern&lt;/h1&gt;
&lt;p&gt;One of the most common mistakes in Elixir systems is turning a GenServer into a &lt;strong&gt;global bottleneck&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;handle_call&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:fetch_url&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_from&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;HTTP&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:reply&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Problem:&lt;/p&gt;
&lt;p&gt;The GenServer becomes responsible for slow work such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP requests&lt;/li&gt;
&lt;li&gt;file IO&lt;/li&gt;
&lt;li&gt;database queries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because a GenServer processes &lt;strong&gt;one message at a time&lt;/strong&gt;, every request queues behind the previous one.&lt;/p&gt;
&lt;p&gt;This can severely limit concurrency.&lt;/p&gt;
&lt;h3&gt;Better approach&lt;/h3&gt;
&lt;p&gt;Use the GenServer for &lt;strong&gt;coordination&lt;/strong&gt;, not heavy work.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;handle_cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:fetch_url&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;module&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;fetch_and_store&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the GenServer remains responsive while tasks perform the expensive work.&lt;/p&gt;
&lt;h1&gt;Choosing the right abstraction&lt;/h1&gt;
&lt;p&gt;A simple decision guide:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Recommended abstraction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple shared state&lt;/td&gt;
&lt;td&gt;Agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stateful service or coordination&lt;/td&gt;
&lt;td&gt;GenServer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallel short-lived work&lt;/td&gt;
&lt;td&gt;Task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ultra-fast shared memory&lt;/td&gt;
&lt;td&gt;ETS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming pipelines&lt;/td&gt;
&lt;td&gt;GenStage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message ingestion pipelines&lt;/td&gt;
&lt;td&gt;Broadway&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Elixir provides multiple abstractions for building concurrent systems, each designed for a specific purpose.&lt;/p&gt;
&lt;p&gt;In practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GenServer&lt;/code&gt; is the most common building block&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Task&lt;/code&gt; handles concurrent work&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ETS&lt;/code&gt; provides high-performance shared memory&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Agent&lt;/code&gt; is useful for very small state containers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Choosing the correct abstraction helps avoid bottlenecks and keeps systems simple as they grow.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/http&quot;&gt;#http&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-21T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/understanding-agent-genserver-task-and-ets-in-elixir</id>
    <title>🐥 Understanding Agent, GenServer, Task, and ETS in Elixir</title>
    <updated>2026-03-21T18:00:00Z</updated>
  </entry>
</feed>